diff --git a/.vagrant-puppet/files/etc/icinga2/conf.d/test-config.conf b/.vagrant-puppet/files/etc/icinga2/conf.d/test-config.conf index f25df325b..560fc6a1c 100644 --- a/.vagrant-puppet/files/etc/icinga2/conf.d/test-config.conf +++ b/.vagrant-puppet/files/etc/icinga2/conf.d/test-config.conf @@ -76,7 +76,7 @@ __function createService(service_type, num) { enable_active_checks = (service_type != "pending") vars.check_type = service_type - assign where match("*" + service_type + "*", host.vars.check_config) + assign where service_type in host.vars.check_config } } @@ -102,12 +102,12 @@ __function createHost(checkType, checkConfig, num, checkEnabled) { } __for (num in range(10)) { - createHost("ok", "ok", num, true) - createHost("random", "random,flapping", num, true) - createHost("down", "warning,critical", num, true) - createHost("unreachable", "unknown", num, true) - createHost("pending", "pending", num, false) - createHost("flap", "flapping", num, true) + createHost("ok", [ "ok" ], num, true) + createHost("random", [ "random", "flapping" ], num, true) + createHost("down", [ "warning", "critical" ], num, true) + createHost("unreachable", [ "unknown" ], num, true) + createHost("pending", [ "pending" ], num, false) + createHost("flap", [ "flapping" ], num, true) } // EOF diff --git a/.vagrant-puppet/manifests/default.pp b/.vagrant-puppet/manifests/default.pp index 39b0d967c..117ad0836 100644 --- a/.vagrant-puppet/manifests/default.pp +++ b/.vagrant-puppet/manifests/default.pp @@ -601,27 +601,15 @@ exec { 'create-pgsql-icingaweb-db': require => Service['postgresql'] } -exec { 'populate-icingaweb-mysql-db-accounts': +exec { 'populate-icingaweb-mysql-db-tables': unless => 'mysql -uicingaweb -picingaweb icingaweb -e "SELECT * FROM account;" &> /dev/null', - command => 'mysql -uicingaweb -picingaweb icingaweb < /vagrant/etc/schema/accounts.mysql.sql', + command => 'mysql -uicingaweb -picingaweb icingaweb < /vagrant/etc/schema/mysql.sql', require => [ Exec['create-mysql-icingaweb-db'] ] } -exec { 'populate-icingweba-pgsql-db-accounts': +exec { 'populate-icingweba-pgsql-db-tables': unless => 'psql -U icingaweb -d icingaweb -c "SELECT * FROM account;" &> /dev/null', - command => 'sudo -u postgres psql -U icingaweb -d icingaweb -f /vagrant/etc/schema/accounts.pgsql.sql', - require => [ Exec['create-pgsql-icingaweb-db'] ] -} - -exec { 'populate-icingaweb-mysql-db-preferences': - unless => 'mysql -uicingaweb -picingaweb icingaweb -e "SELECT * FROM preference;" &> /dev/null', - command => 'mysql -uicingaweb -picingaweb icingaweb < /vagrant/etc/schema/preferences.mysql.sql', - require => [ Exec['create-mysql-icingaweb-db'] ] -} - -exec { 'populate-icingweba-pgsql-db-preferences': - unless => 'psql -U icingaweb -d icingaweb -c "SELECT * FROM preference;" &> /dev/null', - command => 'sudo -u postgres psql -U icingaweb -d icingaweb -f /vagrant/etc/schema/preferences.pgsql.sql', + command => 'sudo -u postgres psql -U icingaweb -d icingaweb -f /vagrant/etc/schema/pgsql.sql', require => [ Exec['create-pgsql-icingaweb-db'] ] } diff --git a/application/clicommands/WebCommand.php b/application/clicommands/WebCommand.php index 11e7f002a..5d9b20c64 100644 --- a/application/clicommands/WebCommand.php +++ b/application/clicommands/WebCommand.php @@ -4,6 +4,7 @@ namespace Icinga\Clicommands; +use Icinga\Application\Icinga; use Icinga\Cli\Command; use Icinga\Exception\IcingaException; @@ -30,7 +31,7 @@ class WebCommand extends Command // throw new IcingaException('Socket is required'); } if ($basedir === null) { - $basedir = dirname(ICINGAWEB_APPDIR) . '/public'; + $basedir = Icinga::app()->getBaseDir('public'); if (! file_exists($basedir) || ! is_dir($basedir)) { throw new IcingaException('Basedir is required'); } diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php index 8b2bb3715..441a84dee 100644 --- a/application/controllers/AuthenticationController.php +++ b/application/controllers/AuthenticationController.php @@ -9,7 +9,7 @@ use Icinga\Web\Controller\ActionController; use Icinga\Form\Authentication\LoginForm; use Icinga\Authentication\AuthChain; use Icinga\Application\Config; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Exception\AuthenticationException; use Icinga\Exception\NotReadableError; use Icinga\Exception\ConfigurationError; @@ -33,6 +33,17 @@ class AuthenticationController extends ActionController */ public function loginAction() { + if (@file_exists(Config::$configDir . '/setup.token')) { + try { + $config = Config::app()->toArray(); + if (empty($config)) { + $this->redirectNow(Url::fromPath('setup')); + } + } catch (NotReadableError $e) { + // Gets thrown in case of insufficient permission only + } + } + $auth = $this->Auth(); $this->view->form = $form = new LoginForm(); $this->view->title = $this->translate('Icingaweb Login'); @@ -93,30 +104,30 @@ class AuthenticationController extends ActionController } } if ($backendsTried === 0) { - throw new ConfigurationError( + $this->view->form->addError( $this->translate( 'No authentication methods available. Did you create' - . ' authentication.ini when installing Icinga Web 2?' + . ' authentication.ini when setting up Icinga Web 2?' ) ); - } - if ($backendsTried === $backendsWithError) { - throw new ConfigurationError( + } else if ($backendsTried === $backendsWithError) { + $this->view->form->addError( $this->translate( 'All configured authentication methods failed.' . ' Please check the system log or Icinga Web 2 log for more information.' ) ); - } - if ($backendsWithError) { - $this->view->form->getElement('username')->addError( + } elseif ($backendsWithError) { + $this->view->form->addError( $this->translate( 'Please note that not all authentication methods were available.' . ' Check the system log or Icinga Web 2 log for more information.' ) ); } - $this->view->form->getElement('password')->addError($this->translate('Incorrect username or password')); + if ($backendsTried > 0 && $backendsTried !== $backendsWithError) { + $this->view->form->getElement('password')->addError($this->translate('Incorrect username or password')); + } } elseif ($request->isGet()) { $user = new User(''); foreach ($chain as $backend) { @@ -134,6 +145,8 @@ class AuthenticationController extends ActionController } catch (Exception $e) { $this->view->errorInfo = $e->getMessage(); } + + $this->view->configMissing = is_dir(Config::$configDir) === false; } /** diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index c12860e72..a681c4255 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -7,7 +7,7 @@ use Icinga\Web\Notification; use Icinga\Application\Modules\Module; use Icinga\Web\Widget; use Icinga\Application\Icinga; -use Icinga\Application\Config as IcingaConfig; +use Icinga\Application\Config; use Icinga\Form\Config\GeneralConfigForm; use Icinga\Form\Config\AuthenticationBackendReorderForm; use Icinga\Form\Config\AuthenticationBackendConfigForm; @@ -46,7 +46,7 @@ class ConfigController extends ActionController public function indexAction() { $form = new GeneralConfigForm(); - $form->setIniConfig(IcingaConfig::app()); + $form->setIniConfig(Config::app()); $form->handleRequest(); $this->view->form = $form; @@ -99,7 +99,7 @@ class ConfigController extends ActionController Notification::success(sprintf($this->translate('Module "%s" enabled'), $module)); $this->rerenderLayout()->reloadCss()->redirectNow('config/modules'); } catch (Exception $e) { - $this->view->exceptionMesssage = $e->getMessage(); + $this->view->exceptionMessage = $e->getMessage(); $this->view->moduleName = $module; $this->view->action = 'enable'; $this->render('module-configuration-error'); @@ -131,7 +131,7 @@ class ConfigController extends ActionController public function authenticationAction() { $form = new AuthenticationBackendReorderForm(); - $form->setIniConfig(IcingaConfig::app('authentication')); + $form->setIniConfig(Config::app('authentication')); $form->handleRequest(); $this->view->form = $form; @@ -145,7 +145,7 @@ class ConfigController extends ActionController public function createauthenticationbackendAction() { $form = new AuthenticationBackendConfigForm(); - $form->setIniConfig(IcingaConfig::app('authentication')); + $form->setIniConfig(Config::app('authentication')); $form->setResourceConfig(ResourceFactory::getResourceConfigs()); $form->setRedirectUrl('config/authentication'); $form->handleRequest(); @@ -161,7 +161,7 @@ class ConfigController extends ActionController public function editauthenticationbackendAction() { $form = new AuthenticationBackendConfigForm(); - $form->setIniConfig(IcingaConfig::app('authentication')); + $form->setIniConfig(Config::app('authentication')); $form->setResourceConfig(ResourceFactory::getResourceConfigs()); $form->setRedirectUrl('config/authentication'); $form->handleRequest(); @@ -179,7 +179,7 @@ class ConfigController extends ActionController $form = new ConfirmRemovalForm(array( 'onSuccess' => function ($request) { $configForm = new AuthenticationBackendConfigForm(); - $configForm->setIniConfig(IcingaConfig::app('authentication')); + $configForm->setIniConfig(Config::app('authentication')); $authBackend = $request->getQuery('auth_backend'); try { @@ -212,7 +212,7 @@ class ConfigController extends ActionController */ public function resourceAction() { - $this->view->resources = IcingaConfig::app('resources', true)->toArray(); + $this->view->resources = Config::app('resources', true)->toArray(); $this->view->tabs->activate('resources'); } @@ -222,7 +222,7 @@ class ConfigController extends ActionController public function createresourceAction() { $form = new ResourceConfigForm(); - $form->setIniConfig(IcingaConfig::app('resources')); + $form->setIniConfig(Config::app('resources')); $form->setRedirectUrl('config/resource'); $form->handleRequest(); @@ -236,7 +236,7 @@ class ConfigController extends ActionController public function editresourceAction() { $form = new ResourceConfigForm(); - $form->setIniConfig(IcingaConfig::app('resources')); + $form->setIniConfig(Config::app('resources')); $form->setRedirectUrl('config/resource'); $form->handleRequest(); @@ -252,7 +252,7 @@ class ConfigController extends ActionController $form = new ConfirmRemovalForm(array( 'onSuccess' => function ($request) { $configForm = new ResourceConfigForm(); - $configForm->setIniConfig(IcingaConfig::app('resources')); + $configForm->setIniConfig(Config::app('resources')); $resource = $request->getQuery('resource'); try { @@ -274,7 +274,7 @@ class ConfigController extends ActionController // Check if selected resource is currently used for authentication $resource = $this->getRequest()->getQuery('resource'); - $authConfig = IcingaConfig::app('authentication')->toArray(); + $authConfig = Config::app('authentication')->toArray(); foreach ($authConfig as $backendName => $config) { if (array_key_exists('resource', $config) && $config['resource'] === $resource) { $form->addError(sprintf( diff --git a/application/controllers/DashboardController.php b/application/controllers/DashboardController.php index 4942f5cdb..51435d727 100644 --- a/application/controllers/DashboardController.php +++ b/application/controllers/DashboardController.php @@ -2,16 +2,16 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -use Icinga\Web\Url; -use Icinga\Logger\Logger; -use Icinga\Config\PreservingIniWriter; -use Icinga\Application\Config as IcingaConfig; -use Icinga\Web\Widget\Dashboard; -use Icinga\Form\Dashboard\AddUrlForm; -use Icinga\Exception\NotReadableError; +use Icinga\Application\Config; +use Icinga\Application\Logger; use Icinga\Exception\ConfigurationError; -use Icinga\Web\Controller\ActionController; use Icinga\Exception\IcingaException; +use Icinga\Exception\NotReadableError; +use Icinga\File\Ini\IniWriter; +use Icinga\Form\Dashboard\AddUrlForm; +use Icinga\Web\Controller\ActionController; +use Icinga\Web\Url; +use Icinga\Web\Widget\Dashboard; /** * Handle creation, removal and displaying of dashboards, panes and components @@ -36,7 +36,7 @@ class DashboardController extends ActionController { $dashboard = new Dashboard(); try { - $dashboardConfig = IcingaConfig::app($config); + $dashboardConfig = Config::app($config); if (count($dashboardConfig) === 0) { return null; } @@ -92,8 +92,8 @@ class DashboardController extends ActionController ltrim($form->getValue('url'), '/') ); - $configFile = IcingaConfig::app('dashboard/dashboard')->getConfigFile(); - if ($this->writeConfiguration(new Zend_Config($dashboard->toArray()), $configFile)) { + $configFile = Config::app('dashboard/dashboard')->getConfigFile(); + if ($this->writeConfiguration(new Config($dashboard->toArray()), $configFile)) { $this->redirectNow(Url::fromPath('dashboard', array('pane' => $form->getValue('pane')))); } else { $this->render('showConfiguration'); @@ -125,7 +125,7 @@ class DashboardController extends ActionController $dashboard->activate($pane); } - $this->view->configPath = IcingaConfig::resolvePath(self::DEFAULT_CONFIG); + $this->view->configPath = Config::resolvePath(self::DEFAULT_CONFIG); if ($dashboard === null) { $this->view->title = 'Dashboard'; @@ -151,14 +151,14 @@ class DashboardController extends ActionController /** * Store the given configuration as INI file * - * @param Zend_Config $config The configuration to store - * @param string $target The path where to store the configuration + * @param Config $config The configuration to store + * @param string $target The path where to store the configuration * * @return bool Whether the configuartion has been successfully stored */ - protected function writeConfiguration(Zend_Config $config, $target) + protected function writeConfiguration(Config $config, $target) { - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); try { $writer->write(); diff --git a/application/controllers/ErrorController.php b/application/controllers/ErrorController.php index 074e2fdc2..23051bd9a 100644 --- a/application/controllers/ErrorController.php +++ b/application/controllers/ErrorController.php @@ -4,7 +4,7 @@ // namespace Icinga\Application\Controllers; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Web\Controller\ActionController; use Icinga\Application\Icinga; diff --git a/application/controllers/FilterController.php b/application/controllers/FilterController.php index 2ffa869db..09e8f0368 100644 --- a/application/controllers/FilterController.php +++ b/application/controllers/FilterController.php @@ -4,7 +4,7 @@ use Icinga\Web\Controller\ActionController; use Icinga\Filter\Filter; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; /** * Application wide interface for filtering diff --git a/application/controllers/ListController.php b/application/controllers/ListController.php index 004c52148..8efc3fc11 100644 --- a/application/controllers/ListController.php +++ b/application/controllers/ListController.php @@ -3,11 +3,9 @@ // {{{ICINGA_LICENSE_HEADER}}} use Icinga\Module\Monitoring\Controller; -use Icinga\Web\Hook; use Icinga\Web\Url; -use Icinga\Data\ResourceFactory; -use Icinga\Logger\Logger; -use Icinga\Logger\Writer\FileWriter; +use Icinga\Application\Logger; +use Icinga\Application\Config; use Icinga\Protocol\File\FileReader; use \Zend_Controller_Action_Exception as ActionError; @@ -50,7 +48,7 @@ class ListController extends Controller . ' - (?.*)$/'; // message $loggerWriter = Logger::getInstance()->getWriter(); - $resource = new FileReader(new Zend_Config(array( + $resource = new FileReader(new Config(array( 'filename' => $loggerWriter->getPath(), 'fields' => $pattern ))); diff --git a/application/controllers/StaticController.php b/application/controllers/StaticController.php index c8b1d9b73..fc5f1354f 100644 --- a/application/controllers/StaticController.php +++ b/application/controllers/StaticController.php @@ -4,7 +4,7 @@ use Icinga\Web\Controller\ActionController; use Icinga\Application\Icinga; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Web\FileCache; use Zend_Controller_Action_Exception as ActionException; diff --git a/application/forms/Config/Authentication/AutologinBackendForm.php b/application/forms/Config/Authentication/AutologinBackendForm.php index 4f4df73e3..0a53de6bd 100644 --- a/application/forms/Config/Authentication/AutologinBackendForm.php +++ b/application/forms/Config/Authentication/AutologinBackendForm.php @@ -31,7 +31,9 @@ class AutologinBackendForm extends Form array( 'required' => true, 'label' => t('Backend Name'), - 'description' => t('The name of this authentication backend'), + 'description' => t( + 'The name of this authentication provider that is used to differentiate it from others' + ), 'validators' => array( array( 'Regex', @@ -50,9 +52,8 @@ class AutologinBackendForm extends Form 'text', 'strip_username_regexp', array( - 'required' => true, - 'label' => t('Backend Domain Pattern'), - 'description' => t('The domain pattern of this authentication backend'), + 'label' => t('Filter Pattern'), + 'description' => t('The regular expression to use to strip specific parts off from usernames. Leave empty if you do not want to strip off anything'), 'value' => '/\@[^$]+$/', 'validators' => array( new Zend_Validate_Callback(function ($value) { @@ -82,7 +83,7 @@ class AutologinBackendForm extends Form * * @return bool Whether validation succeeded or not */ - public function isValidAuthenticationBackend(Form $form) + public static function isValidAuthenticationBackend(Form $form) { return true; } diff --git a/application/forms/Config/Authentication/DbBackendForm.php b/application/forms/Config/Authentication/DbBackendForm.php index 23335f8fa..0ffaa2a1e 100644 --- a/application/forms/Config/Authentication/DbBackendForm.php +++ b/application/forms/Config/Authentication/DbBackendForm.php @@ -5,6 +5,7 @@ namespace Icinga\Form\Config\Authentication; use Exception; +use Icinga\Application\Config; use Icinga\Web\Form; use Icinga\Web\Request; use Icinga\Data\ResourceFactory; @@ -54,7 +55,9 @@ class DbBackendForm extends Form array( 'required' => true, 'label' => t('Backend Name'), - 'description' => t('The name of this authentication provider'), + 'description' => t( + 'The name of this authentication provider that is used to differentiate it from others' + ), ) ); $this->addElement( @@ -88,7 +91,7 @@ class DbBackendForm extends Form */ public function onSuccess(Request $request) { - if (false === $this->isValidAuthenticationBackend($this)) { + if (false === static::isValidAuthenticationBackend($this)) { return false; } } @@ -100,21 +103,29 @@ class DbBackendForm extends Form * * @return bool Whether validation succeeded or not */ - public function isValidAuthenticationBackend(Form $form) + public static function isValidAuthenticationBackend(Form $form) { - $element = $form->getElement('resource'); - try { - $dbUserBackend = new DbUserBackend(ResourceFactory::create($element->getValue())); + $dbUserBackend = new DbUserBackend(ResourceFactory::createResource($form->getResourceConfig())); if ($dbUserBackend->count() < 1) { - $element->addError(t('No users found under the specified database backend')); + $form->addError(t('No users found under the specified database backend')); return false; } } catch (Exception $e) { - $element->addError(sprintf(t('Using the specified backend failed: %s'), $e->getMessage())); + $form->addError(sprintf(t('Using the specified backend failed: %s'), $e->getMessage())); return false; } return true; } + + /** + * Return the configuration for the chosen resource + * + * @return Config + */ + public function getResourceConfig() + { + return ResourceFactory::getResourceConfig($this->getValue('resource')); + } } diff --git a/application/forms/Config/Authentication/LdapBackendForm.php b/application/forms/Config/Authentication/LdapBackendForm.php index 6056b0911..84298e3c6 100644 --- a/application/forms/Config/Authentication/LdapBackendForm.php +++ b/application/forms/Config/Authentication/LdapBackendForm.php @@ -5,9 +5,11 @@ namespace Icinga\Form\Config\Authentication; use Exception; +use Icinga\Application\Config; use Icinga\Web\Form; use Icinga\Web\Request; use Icinga\Data\ResourceFactory; +use Icinga\Exception\AuthenticationException; use Icinga\Authentication\Backend\LdapUserBackend; /** @@ -54,7 +56,9 @@ class LdapBackendForm extends Form array( 'required' => true, 'label' => t('Backend Name'), - 'description' => t('The name of this authentication backend') + 'description' => t( + 'The name of this authentication provider that is used to differentiate it from others' + ) ) ); $this->addElement( @@ -97,7 +101,16 @@ class LdapBackendForm extends Form 'value' => 'ldap' ) ); - + $this->addElement( + 'text', + 'base_dn', + array( + 'required' => false, + 'label' => t('Base DN'), + 'description' => t('The path where users can be found on the ldap server. ' . + ' Leave empty to select all users available on the specified resource.') + ) + ); return $this; } @@ -108,7 +121,7 @@ class LdapBackendForm extends Form */ public function onSuccess(Request $request) { - if (false === $this->isValidAuthenticationBackend($this)) { + if (false === static::isValidAuthenticationBackend($this)) { return false; } } @@ -120,22 +133,34 @@ class LdapBackendForm extends Form * * @return bool Whether validation succeeded or not */ - public function isValidAuthenticationBackend(Form $form) + public static function isValidAuthenticationBackend(Form $form) { - $element = $form->getElement('resource'); - try { $ldapUserBackend = new LdapUserBackend( - ResourceFactory::create($element->getValue()), + ResourceFactory::createResource($form->getResourceConfig()), $form->getElement('user_class')->getValue(), - $form->getElement('user_name_attribute')->getValue() + $form->getElement('user_name_attribute')->getValue(), + $form->getElement('base_dn')->getValue() ); $ldapUserBackend->assertAuthenticationPossible(); + } catch (AuthenticationException $e) { + $form->addError($e->getMessage()); + return false; } catch (Exception $e) { - $element->addError(sprintf(t('Connection validation failed: %s'), $e->getMessage())); + $form->addError(sprintf(t('Unable to validate authentication: %s'), $e->getMessage())); return false; } return true; } + + /** + * Return the configuration for the chosen resource + * + * @return Config + */ + public function getResourceConfig() + { + return ResourceFactory::getResourceConfig($this->getValue('resource')); + } } diff --git a/application/forms/Config/AuthenticationBackendConfigForm.php b/application/forms/Config/AuthenticationBackendConfigForm.php index e81382cd4..33e0b1a3a 100644 --- a/application/forms/Config/AuthenticationBackendConfigForm.php +++ b/application/forms/Config/AuthenticationBackendConfigForm.php @@ -10,6 +10,7 @@ use Icinga\Form\ConfigForm; use Icinga\Web\Notification; use Icinga\Application\Config; use Icinga\Application\Platform; +use Icinga\Data\ResourceFactory; use Icinga\Exception\ConfigurationError; use Icinga\Form\Config\Authentication\DbBackendForm; use Icinga\Form\Config\Authentication\LdapBackendForm; @@ -39,8 +40,6 @@ class AuthenticationBackendConfigForm extends ConfigForm * @param Config $resources The resource configuration * * @return self - * - * @throws ConfigurationError In case no resources are available for authentication */ public function setResourceConfig(Config $resourceConfig) { @@ -49,10 +48,6 @@ class AuthenticationBackendConfigForm extends ConfigForm $resources[strtolower($resource->type)][] = $name; } - if (empty($resources)) { - throw new ConfigurationError(t('Could not find any resources for authentication')); - } - $this->resources = $resources; return $this; } @@ -201,7 +196,7 @@ class AuthenticationBackendConfigForm extends ConfigForm { if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) { $backendForm = $this->getBackendForm($this->getElement('type')->getValue()); - if (false === $backendForm->isValidAuthenticationBackend($this)) { + if (false === $backendForm::isValidAuthenticationBackend($this)) { $this->addElement($this->getForceCreationCheckbox()); return false; } @@ -251,6 +246,17 @@ class AuthenticationBackendConfigForm extends ConfigForm $configValues['type'] = $configValues['backend']; $configValues['name'] = $authBackend; $this->populate($configValues); + } elseif (empty($this->resources)) { + $autologinBackends = array_filter( + $this->config->toArray(), + function ($authBackendCfg) { + return isset($authBackendCfg['backend']) && $authBackendCfg['backend'] === 'autologin'; + } + ); + + if (false === empty($autologinBackends)) { + throw new ConfigurationError(t('Could not find any resources for authentication')); + } } } @@ -280,7 +286,7 @@ class AuthenticationBackendConfigForm extends ConfigForm public function createElements(array $formData) { $backendTypes = array(); - $backendType = isset($formData['type']) ? $formData['type'] : 'db'; + $backendType = isset($formData['type']) ? $formData['type'] : null; if (isset($this->resources['db'])) { $backendTypes['db'] = t('Database'); @@ -299,6 +305,10 @@ class AuthenticationBackendConfigForm extends ConfigForm $backendTypes['autologin'] = t('Autologin'); } + if ($backendType === null) { + $backendType = key($backendTypes); + } + $this->addElement( 'select', 'type', @@ -307,7 +317,7 @@ class AuthenticationBackendConfigForm extends ConfigForm 'required' => true, 'autosubmit' => true, 'label' => t('Backend Type'), - 'description' => t('The type of the resource to use for this authenticaton backend'), + 'description' => t('The type of the resource to use for this authenticaton provider'), 'multiOptions' => $backendTypes ) ); @@ -319,4 +329,14 @@ class AuthenticationBackendConfigForm extends ConfigForm $this->addElements($this->getBackendForm($backendType)->createElements($formData)->getElements()); } + + /** + * Return the configuration for the chosen resource + * + * @return Config + */ + public function getResourceConfig() + { + return ResourceFactory::getResourceConfig($this->getValue('resource')); + } } diff --git a/application/forms/Config/General/ApplicationConfigForm.php b/application/forms/Config/General/ApplicationConfigForm.php index fda695886..6a35074d2 100644 --- a/application/forms/Config/General/ApplicationConfigForm.php +++ b/application/forms/Config/General/ApplicationConfigForm.php @@ -5,9 +5,11 @@ namespace Icinga\Form\Config\General; use DateTimeZone; -use Icinga\Web\Form; -use Icinga\Util\Translator; +use Icinga\Application\Icinga; use Icinga\Data\ResourceFactory; +use Icinga\Util\Translator; +use Icinga\Web\Form; + /** * Form class to modify the general application configuration @@ -71,12 +73,12 @@ class ApplicationConfigForm extends Form array( 'label' => t('Module Path'), 'required' => true, + 'value' => implode(':', Icinga::app()->getModuleManager()->getModuleDirs()), 'description' => t( 'Contains the directories that will be searched for available modules, separated by ' . 'colons. Modules that don\'t exist in these directories can still be symlinked in ' . 'the module folder, but won\'t show up in the list of disabled modules.' - ), - 'value' => realpath(ICINGAWEB_APPDIR . '/../modules') + ) ) ); diff --git a/application/forms/Config/General/LoggingConfigForm.php b/application/forms/Config/General/LoggingConfigForm.php index 733c306b9..bddb648cd 100644 --- a/application/forms/Config/General/LoggingConfigForm.php +++ b/application/forms/Config/General/LoggingConfigForm.php @@ -5,7 +5,7 @@ namespace Icinga\Form\Config\General; use Icinga\Application\Icinga; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Web\Form; use Icinga\Web\Form\Validator\WritablePathValidator; @@ -30,13 +30,13 @@ class LoggingConfigForm extends Form 'logging_log', array( 'required' => true, - 'class' => 'autosubmit', + 'autosubmit' => true, 'label' => t('Logging Type'), 'description' => t('The type of logging to utilize.'), 'multiOptions' => array( 'syslog' => 'Syslog', - 'file' => t('File'), - 'none' => t('None') + 'file' => t('File', 'app.config.logging.type'), + 'none' => t('None', 'app.config.logging.type') ) ) ); @@ -50,16 +50,16 @@ class LoggingConfigForm extends Form 'label' => t('Logging Level'), 'description' => t('The maximum logging level to emit.'), 'multiOptions' => array( - Logger::$levels[Logger::ERROR] => t('Error'), - Logger::$levels[Logger::WARNING] => t('Warning'), - Logger::$levels[Logger::INFO] => t('Information'), - Logger::$levels[Logger::DEBUG] => t('Debug') + Logger::$levels[Logger::ERROR] => t('Error', 'app.config.logging.level'), + Logger::$levels[Logger::WARNING] => t('Warning', 'app.config.logging.level'), + Logger::$levels[Logger::INFO] => t('Information', 'app.config.logging.level'), + Logger::$levels[Logger::DEBUG] => t('Debug', 'app.config.logging.level') ) ) ); } - if (isset($formData['logging_log']) && $formData['logging_log'] === 'syslog') { + if (false === isset($formData['logging_log']) || $formData['logging_log'] === 'syslog') { $this->addElement( 'text', 'logging_application', diff --git a/application/forms/Config/Resource/DbResourceForm.php b/application/forms/Config/Resource/DbResourceForm.php index 4fd7297d9..4c0ce7740 100644 --- a/application/forms/Config/Resource/DbResourceForm.php +++ b/application/forms/Config/Resource/DbResourceForm.php @@ -5,11 +5,12 @@ namespace Icinga\Form\Config\Resource; use Exception; -use Zend_Config; +use Icinga\Application\Config; use Icinga\Web\Form; use Icinga\Web\Request; use Icinga\Web\Form\Element\Number; use Icinga\Data\ResourceFactory; +use Icinga\Application\Platform; /** * Form class for adding/modifying database resources @@ -29,6 +30,23 @@ class DbResourceForm extends Form */ public function createElements(array $formData) { + $dbChoices = array(); + if (Platform::zendClassExists('Zend_Db_Adapter_Pdo_Mysql')) { + $dbChoices['mysql'] = 'MySQL'; + } + if (Platform::zendClassExists('Zend_Db_Adapter_Pdo_Pgsql')) { + $dbChoices['pgsql'] = 'PostgreSQL'; + } + + $this->addElement( + 'text', + 'name', + array( + 'required' => true, + 'label' => t('Resource Name'), + 'description' => t('The unique name of this resource') + ) + ); $this->addElement( 'select', 'db', @@ -36,11 +54,7 @@ class DbResourceForm extends Form 'required' => true, 'label' => t('Database Type'), 'description' => t('The type of SQL database'), - 'multiOptions' => array( - 'mysql' => 'MySQL', - 'pgsql' => 'PostgreSQL' - //'oracle' => 'Oracle' - ) + 'multiOptions' => $dbChoices ) ); $this->addElement( @@ -103,7 +117,7 @@ class DbResourceForm extends Form */ public function onSuccess(Request $request) { - if (false === $this->isValidResource($this)) { + if (false === static::isValidResource($this)) { return false; } } @@ -115,10 +129,10 @@ class DbResourceForm extends Form * * @return bool Whether validation succeeded or not */ - public function isValidResource(Form $form) + public static function isValidResource(Form $form) { try { - $resource = ResourceFactory::createResource(new Zend_Config($form->getValues())); + $resource = ResourceFactory::createResource(new Config($form->getValues())); $resource->getConnection()->getConnection(); } catch (Exception $e) { $form->addError(t('Connectivity validation failed, connection to the given resource not possible.')); diff --git a/application/forms/Config/Resource/FileResourceForm.php b/application/forms/Config/Resource/FileResourceForm.php index d1d33a749..2814e9a72 100644 --- a/application/forms/Config/Resource/FileResourceForm.php +++ b/application/forms/Config/Resource/FileResourceForm.php @@ -25,6 +25,15 @@ class FileResourceForm extends Form */ public function createElements(array $formData) { + $this->addElement( + 'text', + 'name', + array( + 'required' => true, + 'label' => t('Resource Name'), + 'description' => t('The unique name of this resource') + ) + ); $this->addElement( 'text', 'filename', diff --git a/application/forms/Config/Resource/LdapResourceForm.php b/application/forms/Config/Resource/LdapResourceForm.php index 6196f3a17..cd482a7fb 100644 --- a/application/forms/Config/Resource/LdapResourceForm.php +++ b/application/forms/Config/Resource/LdapResourceForm.php @@ -5,7 +5,7 @@ namespace Icinga\Form\Config\Resource; use Exception; -use Zend_Config; +use Icinga\Application\Config; use Icinga\Web\Form; use Icinga\Web\Request; use Icinga\Web\Form\Element\Number; @@ -29,6 +29,15 @@ class LdapResourceForm extends Form */ public function createElements(array $formData) { + $this->addElement( + 'text', + 'name', + array( + 'required' => true, + 'label' => t('Resource Name'), + 'description' => t('The unique name of this resource') + ) + ); $this->addElement( 'text', 'hostname', @@ -56,7 +65,7 @@ class LdapResourceForm extends Form array( 'required' => true, 'label' => t('Root DN'), - 'description' => t('The path where users can be found on the ldap server') + 'description' => t('Only the root and its child nodes will be accessible on this resource.') ) ); $this->addElement( @@ -89,7 +98,7 @@ class LdapResourceForm extends Form */ public function onSuccess(Request $request) { - if (false === $this->isValidResource($this)) { + if (false === static::isValidResource($this)) { return false; } } @@ -101,11 +110,17 @@ class LdapResourceForm extends Form * * @return bool Whether validation succeeded or not */ - public function isValidResource(Form $form) + public static function isValidResource(Form $form) { try { - $resource = ResourceFactory::createResource(new Zend_Config($form->getValues())); - $resource->connect(); + $resource = ResourceFactory::createResource(new Config($form->getValues())); + if (false === $resource->testCredentials( + $form->getElement('bind_dn')->getValue(), + $form->getElement('bind_pw')->getValue() + ) + ) { + throw new Exception(); + } } catch (Exception $e) { $form->addError(t('Connectivity validation failed, connection to the given resource not possible.')); return false; diff --git a/application/forms/Config/Resource/LivestatusResourceForm.php b/application/forms/Config/Resource/LivestatusResourceForm.php index 92534f220..27925e370 100644 --- a/application/forms/Config/Resource/LivestatusResourceForm.php +++ b/application/forms/Config/Resource/LivestatusResourceForm.php @@ -5,7 +5,7 @@ namespace Icinga\Form\Config\Resource; use Exception; -use Zend_Config; +use Icinga\Application\Config; use Icinga\Web\Form; use Icinga\Web\Request; use Icinga\Application\Icinga; @@ -29,6 +29,15 @@ class LivestatusResourceForm extends Form */ public function createElements(array $formData) { + $this->addElement( + 'text', + 'name', + array( + 'required' => true, + 'label' => t('Resource Name'), + 'description' => t('The unique name of this resource') + ) + ); $this->addElement( 'text', 'socket', @@ -50,7 +59,7 @@ class LivestatusResourceForm extends Form */ public function onSuccess(Request $request) { - if (false === $this->isValidResource($this)) { + if (false === static::isValidResource($this)) { return false; } } @@ -62,10 +71,10 @@ class LivestatusResourceForm extends Form * * @return bool Whether validation succeeded or not */ - public function isValidResource(Form $form) + public static function isValidResource(Form $form) { try { - $resource = ResourceFactory::createResource(new Zend_Config($form->getValues())); + $resource = ResourceFactory::createResource(new Config($form->getValues())); $resource->connect()->disconnect(); } catch (Exception $e) { $form->addError(t('Connectivity validation failed, connection to the given resource not possible.')); diff --git a/application/forms/Config/ResourceConfigForm.php b/application/forms/Config/ResourceConfigForm.php index 1aa7edc96..a269885cf 100644 --- a/application/forms/Config/ResourceConfigForm.php +++ b/application/forms/Config/ResourceConfigForm.php @@ -132,7 +132,7 @@ class ResourceConfigForm extends ConfigForm { if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) { $resourceForm = $this->getResourceForm($this->getElement('type')->getValue()); - if (method_exists($resourceForm, 'isValidResource') && false === $resourceForm->isValidResource($this)) { + if (method_exists($resourceForm, 'isValidResource') && false === $resourceForm::isValidResource($this)) { $this->addElement($this->getForceCreationCheckbox()); return false; } @@ -220,15 +220,6 @@ class ResourceConfigForm extends ConfigForm $resourceTypes['db'] = t('SQL Database'); } - $this->addElement( - 'text', - 'name', - array( - 'required' => true, - 'label' => t('Resource Name'), - 'description' => t('The unique name of this resource') - ) - ); $this->addElement( 'select', 'type', diff --git a/application/forms/ConfigForm.php b/application/forms/ConfigForm.php index 86790d618..d1f2ed1dd 100644 --- a/application/forms/ConfigForm.php +++ b/application/forms/ConfigForm.php @@ -7,7 +7,7 @@ namespace Icinga\Form; use Exception; use Icinga\Web\Form; use Icinga\Application\Config; -use Icinga\Config\PreservingIniWriter; +use Icinga\File\Ini\IniWriter; /** * Form base-class providing standard functionality for configuration forms @@ -43,7 +43,7 @@ class ConfigForm extends Form */ public function save() { - $writer = new PreservingIniWriter( + $writer = new IniWriter( array( 'config' => $this->config, 'filename' => $this->config->getConfigFile() diff --git a/application/forms/Dashboard/AddUrlForm.php b/application/forms/Dashboard/AddUrlForm.php index 88195654d..564ff3d7a 100644 --- a/application/forms/Dashboard/AddUrlForm.php +++ b/application/forms/Dashboard/AddUrlForm.php @@ -4,7 +4,7 @@ namespace Icinga\Form\Dashboard; -use Icinga\Application\Config as IcingaConfig; +use Icinga\Application\Config; use Icinga\Web\Widget\Dashboard; use Icinga\Web\Form; @@ -110,7 +110,7 @@ class AddUrlForm extends Form protected function getDashboardPaneSelectionValues() { $dashboard = new Dashboard(); - $dashboard->readConfig(IcingaConfig::app('dashboard/dashboard')); + $dashboard->readConfig(Config::app('dashboard/dashboard')); return $dashboard->getPaneKeyTitleArray(); } } diff --git a/application/forms/LdapDiscoveryForm.php b/application/forms/LdapDiscoveryForm.php new file mode 100644 index 000000000..88f30f2c1 --- /dev/null +++ b/application/forms/LdapDiscoveryForm.php @@ -0,0 +1,213 @@ +setName('form_ldap_discovery'); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + 'text', + 'domain', + array( + 'required' => true, + 'label' => t('Search Domain'), + 'description' => t('Search this domain for records of available servers.'), + ) + ); + + if (false) { + $this->addElement( + new Note( + 'additional_description', + array( + 'value' => t('No Ldap servers found on this domain.' + . ' You can try to specify host and port and try again, or just skip this step and ' + . 'configure the server manually.' + ) + ) + ) + ); + $this->addElement( + 'text', + 'hostname', + array( + 'required' => false, + 'label' => t('Host'), + 'description' => t('IP or host name to search.'), + ) + ); + + $this->addElement( + 'text', + 'port', + array( + 'required' => false, + 'label' => t('Port'), + 'description' => t('Port', 389), + ) + ); + } + return $this; + } + + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + if ($this->discover($this->getValue('domain'))) { + return true; + } + return true; + } + + + private function discover($domain) + { + // Attempt 1: Connect to the domain directly + if ($this->discoverCapabilities(array( + 'hostname' => $domain, + 'port' => 389) + )) { + return true; + } + + // Attempt 2: Discover all available ldap dns records and connect to the first one + $cap = false; + $records = array_merge(Dns::getSrvRecords($domain, 'ldap'), Dns::getSrvRecords($domain, 'ldaps')); + if (isset($records[0])) { + $record = $records[0]; + if (isset($record['port'])) { + $cap = $this->discoverCapabilities(array( + 'hostname' => $record['target'], + 'port' => $record['port'] + )); + } else { + $cap = $this->discoverCapabilities(array( + 'hostname' => $record['target'], + 'port' => 389 + )); + } + } + return $cap; + } + + private function discoverCapabilities($config) + { + $conn = new Connection(new Config($config)); + try { + $conn->connect(); + $this->capabilities = $conn->getCapabilities(); + $this->namingContext = $conn->getDefaultNamingContext(); + $this->port = $config['port']; + $this->domain = $config['hostname']; + return true; + } catch (LdapException $e) { + Logger::info( + 'Ldap discovery for ' . $config['hostname'] . ':' . $config['port'] . ' failed: ' . $e->getMessage() + ); + return false; + } + } + + public function suggestResourceSettings() + { + if (! isset($this->capabilities)) { + return array(); + } + if ($this->capabilities->msCapabilities->ActiveDirectoryOid) { + return array( + 'hostname' => $this->domain, + 'port' => $this->port, + 'root_dn' => $this->namingContext + ); + } else { + return array( + 'hostname' => $this->domain, + 'port' => $this->port, + 'root_dn' => $this->namingContext + ); + } + } + + public function hasSuggestion() + { + return isset($this->capabilities); + } + + public function suggestBackendSettings() + { + if (! isset($this->capabilities)) { + return array(); + } + if ($this->capabilities->msCapabilities->ActiveDirectoryOid) { + return array( + 'base_dn' => $this->namingContext, + 'user_class' => 'user', + 'user_name_attribute' => 'sAMAccountName' + ); + } else { + return array( + 'base_dn' => $this->namingContext, + 'user_class' => 'inetOrgPerson', + 'user_name_attribute' => 'uid' + ); + } + } + + public function isAd() + { + return $this->capabilities->msCapabilities->ActiveDirectoryOid; + } +} \ No newline at end of file diff --git a/application/forms/PreferenceForm.php b/application/forms/PreferenceForm.php index 0b1ba9a9b..dcd20ea4e 100644 --- a/application/forms/PreferenceForm.php +++ b/application/forms/PreferenceForm.php @@ -6,7 +6,7 @@ namespace Icinga\Form; use Exception; use DateTimeZone; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\User\Preferences; use Icinga\User\Preferences\PreferencesStore; use Icinga\Util\TimezoneDetect; @@ -100,10 +100,7 @@ class PreferenceForm extends Form } $this->preferences->icingaweb = $webPreferences; - // TODO: Is this even necessary in case the session is written on response? - $session = Session::getSession(); - $session->user->setPreferences($this->preferences); - $session->write(); + Session::getSession()->user->setPreferences($this->preferences); try { $this->save(); diff --git a/application/views/scripts/authentication/login.phtml b/application/views/scripts/authentication/login.phtml index fe3ab5e3b..5ec605f1b 100644 --- a/application/views/scripts/authentication/login.phtml +++ b/application/views/scripts/authentication/login.phtml @@ -15,5 +15,17 @@ form ?> + +
', // TODO: Documentation link + '', + '' + ); ?>
+ diff --git a/application/views/scripts/joystickPagination.phtml b/application/views/scripts/joystickPagination.phtml new file mode 100644 index 000000000..1808ba7e3 --- /dev/null +++ b/application/views/scripts/joystickPagination.phtml @@ -0,0 +1,99 @@ +count() <= 1 && $yAxisPaginator->count() <= 1) { + return; // Display this pagination only if there are multiple pages +} + +$fromTo = t('%s: %d to %d of %d'); +$xAxisPages = $xAxisPaginator->getPages('all'); +$yAxisPages = $yAxisPaginator->getPages('all'); + +$totalYAxisPages = $yAxisPaginator->count(); +$currentYAxisPage = $yAxisPaginator->getCurrentPageNumber(); +$prevYAxisPage = $currentYAxisPage > 1 ? $currentYAxisPage - 1 : null; +$nextYAxisPage = $currentYAxisPage < $totalYAxisPages ? $currentYAxisPage + 1 : null; + +$totalXAxisPages = $xAxisPaginator->count(); +$currentXAxisPage = $xAxisPaginator->getCurrentPageNumber(); +$prevXAxisPage = $currentXAxisPage > 1 ? $currentXAxisPage - 1 : null; +$nextXAxisPage = $currentXAxisPage < $totalXAxisPages ? $currentXAxisPage + 1 : null; + +?> + + + + + + + + + + + + + + + + + + + +
  + + icon('up_petrol.png'); ?> + + icon('up.png'); ?> + +  
+ + icon('prev_petrol.png'); ?> + + icon('prev.png'); ?> + +   + + icon('next_petrol.png'); ?> + + icon('next.png'); ?> + +
  + + icon('down_petrol.png'); ?> + + icon('down.png'); ?> + +  
\ No newline at end of file diff --git a/bin/icingacli b/bin/icingacli index 1a0781222..cb0d68397 100755 --- a/bin/icingacli +++ b/bin/icingacli @@ -1,6 +1,9 @@ #!/usr/bin/php -dispatch(); + +Icinga\Application\Cli::start()->dispatch(); diff --git a/doc/installation.md b/doc/installation.md index 9de3f7d5c..60a5a3ce5 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -74,8 +74,7 @@ create all database tables. You will find the installation guides for the differ > > RPM packages install the schema into /usr/share/doc/icingaweb-<version>/schema - bash$ mysql -u root -p icingaweb < etc/schema/accounts.mysql.sql - bash$ mysql -u root -p icingaweb < etc/schema/preferences.mysql.sql + bash$ mysql -u root -p icingaweb < etc/schema/mysql.sql #### PostgreSQL @@ -108,8 +107,7 @@ And restart your database ('service postgresql restart' or '/etc/init.d/postgres > > RPM packages install the schema into /usr/share/doc/icingaweb-<version>/schema - bash$ psql -U icingaweb -a -f etc/schema/accounts.pgsql.sql - bash$ psql -U icingaweb -a -f etc/schema/preferences.pgsql.sql + bash$ psql -U icingaweb -a -f etc/schema/pgsql.sql diff --git a/etc/schema/accounts.mysql.sql b/etc/schema/accounts.mysql.sql deleted file mode 100644 index 72eeee4fc..000000000 --- a/etc/schema/accounts.mysql.sql +++ /dev/null @@ -1,24 +0,0 @@ -create table account ( - `username` varchar(255) COLLATE latin1_general_ci NOT NULL, - `salt` varchar(255) NOT NULL, - `password` varchar(255) NOT NULL, - `active` tinyint(1) DEFAULT NULL, - PRIMARY KEY (`username`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - -/* - * user: icingaadmin - * password: icinga - */ -INSERT INTO account ( - `username`, - `salt`, - `password`, - `active` - ) - VALUES ( - 'icingaadmin', - '57cfd5746224be4f60c25d4e8514bec35ad2d01810723a138756b285898e71b2', - '43f8e0588eb39f1a41383b48def0b1fdc45e79b8f67194cccee4453eb3f4ea13', - 1 - ); diff --git a/etc/schema/accounts.pgsql.sql b/etc/schema/accounts.pgsql.sql deleted file mode 100644 index 84ecb54e8..000000000 --- a/etc/schema/accounts.pgsql.sql +++ /dev/null @@ -1,28 +0,0 @@ -create table "account" ( - "username" character varying(255) NOT NULL, - "salt" character varying(255), - "password" character varying(255) NOT NULL, - "active" boolean -); - -ALTER TABLE ONLY "account" - ADD CONSTRAINT account_pkey PRIMARY KEY ("username"); - -CREATE UNIQUE INDEX username_lower_unique_idx ON "account" USING btree (lower((username)::text)); - -/* - * user: icingaadmin - * password: icinga - */ -INSERT INTO "account" ( - "username", - "salt", - "password", - "active" - ) - VALUES ( - 'icingaadmin', - '57cfd5746224be4f60c25d4e8514bec35ad2d01810723a138756b285898e71b2', - '43f8e0588eb39f1a41383b48def0b1fdc45e79b8f67194cccee4453eb3f4ea13', - true - ); diff --git a/etc/schema/pgsql.schema.sql b/etc/schema/pgsql.schema.sql new file mode 100644 index 000000000..b699eccf8 --- /dev/null +++ b/etc/schema/pgsql.schema.sql @@ -0,0 +1,96 @@ +/** + * Table "icingaweb_group" + */ +CREATE TABLE "icingaweb_group" ( + "name" character varying(64) NOT NULL, + "parent" character varying(64) NULL DEFAULT NULL, + "ctime" timestamp NULL DEFAULT NULL, + "mtime" timestamp NULL DEFAULT NULL +); + +ALTER TABLE ONLY "icingaweb_group" + ADD CONSTRAINT pk_icingaweb_group + PRIMARY KEY ( + "name" +); + +CREATE UNIQUE INDEX idx_icingaweb_group + ON "icingaweb_group" + USING btree ( + lower((name)::text) +); + + +/** + * Table "icingaweb_group_membership" + */ +CREATE TABLE "icingaweb_group_membership" ( + "group_name" character varying(64) NOT NULL, + "username" character varying(64) NOT NULL, + "ctime" timestamp NULL DEFAULT NULL, + "mtime" timestamp NULL DEFAULT NULL +); + +ALTER TABLE ONLY "icingaweb_group_membership" + ADD CONSTRAINT pk_icingaweb_group_membership + PRIMARY KEY ( + "group_name", + "username" +); + +CREATE UNIQUE INDEX idx_icingaweb_group_membership + ON "icingaweb_group_membership" + USING btree ( + lower((group_name)::text), + lower((username)::text) +); + + +/** + * Table "icingaweb_user" + */ +CREATE TABLE "icingaweb_user" ( + "name" character varying(64) NOT NULL, + "active" smallint NOT NULL, + "password_hash" bytea NOT NULL, + "ctime" timestamp NULL DEFAULT NULL, + "mtime" timestamp NULL DEFAULT NULL +); + +ALTER TABLE ONLY "icingaweb_user" + ADD CONSTRAINT pk_icingaweb_user + PRIMARY KEY ( + "name" +); + +CREATE UNIQUE INDEX idx_icingaweb_user + ON "icingaweb_user" + USING btree ( + lower((name)::text) +); + + +/** + * Table "icingaweb_user_preference" + */ +CREATE TABLE "icingaweb_user_preference" ( + "username" character varying(64) NOT NULL, + "name" character varying(64) NOT NULL, + "value" character varying(255) NOT NULL, + "ctime" timestamp NULL DEFAULT NULL, + "mtime" timestamp NULL DEFAULT NULL +); + +ALTER TABLE ONLY "icingaweb_user_preference" + ADD CONSTRAINT pk_icingaweb_user_preference + PRIMARY KEY ( + "username", + "name" +); + +CREATE UNIQUE INDEX idx_icingaweb_user_preference + ON "icingaweb_user_preference" + USING btree ( + lower((username)::text), + lower((name)::text) +); \ No newline at end of file diff --git a/etc/schema/preferences.mysql.sql b/etc/schema/preferences.mysql.sql deleted file mode 100644 index 5549daa3f..000000000 --- a/etc/schema/preferences.mysql.sql +++ /dev/null @@ -1,6 +0,0 @@ -create table `preference`( - `username` VARCHAR(255) COLLATE latin1_general_ci NOT NULL, - `key` VARCHAR(100) COLLATE latin1_general_ci NOT NULL, - `value` VARCHAR(255) NOT NULL, - PRIMARY KEY (`username`, `key`) -) ENGINE=InnoDB; \ No newline at end of file diff --git a/etc/schema/preferences.pgsql.sql b/etc/schema/preferences.pgsql.sql deleted file mode 100644 index 8b694f0a0..000000000 --- a/etc/schema/preferences.pgsql.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table "preference"( - "username" VARCHAR(255) NOT NULL, - "key" VARCHAR(100) NOT NULL, - "value" VARCHAR(255) NOT NULL -); - -ALTER TABLE ONLY "preference" - ADD CONSTRAINT preference_pkey PRIMARY KEY ("username", "key"); - -CREATE UNIQUE INDEX username_and_key_lower_unique_idx ON "preference" USING btree (lower((username)::text), lower((key)::text)); \ No newline at end of file diff --git a/library/Icinga/Application/ApplicationBootstrap.php b/library/Icinga/Application/ApplicationBootstrap.php index 958642940..a259496bf 100644 --- a/library/Icinga/Application/ApplicationBootstrap.php +++ b/library/Icinga/Application/ApplicationBootstrap.php @@ -6,14 +6,14 @@ namespace Icinga\Application; use ErrorException; use Exception; -use Zend_Config; use Icinga\Application\Modules\Manager as ModuleManager; use Icinga\Data\ResourceFactory; use Icinga\Exception\ConfigurationError; use Icinga\Exception\NotReadableError; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Util\DateTimeFactory; use Icinga\Util\Translator; +use Icinga\File\Ini\IniWriter; use Icinga\Exception\IcingaException; /** @@ -40,6 +40,43 @@ use Icinga\Exception\IcingaException; */ abstract class ApplicationBootstrap { + /** + * Base directory + * + * Parent folder for at least application, bin, modules, library/vendor and public + * + * @var string + */ + protected $baseDir; + + /** + * Application directory + * + * @var string + */ + protected $appDir; + + /** + * Vendor library directory + * + * @var string + */ + protected $vendorDir; + + /** + * Library directory + * + * @var string + */ + protected $libDir; + + /** + * Configuration directory + * + * @var string + */ + protected $configDir; + /** * Icinga auto loader * @@ -47,34 +84,13 @@ abstract class ApplicationBootstrap */ private $loader; - /** - * Library directory - * - * @var string - */ - private $libDir; - /** * Config object * - * @var Zend_Config + * @var Config */ protected $config; - /** - * Configuration directory - * - * @var string - */ - private $configDir; - - /** - * Application directory - * - * @var string - */ - private $appDir; - /** * Module manager * @@ -98,27 +114,20 @@ abstract class ApplicationBootstrap /** * Constructor + * + * @param string $baseDir Icinga Web 2 base directory + * @param string $configDir Path to Icinga Web 2's configuration files */ - protected function __construct($configDir = null) + protected function __construct($baseDir = null, $configDir = null) { + if ($baseDir === null) { + $baseDir = dirname($this->getBootstrapDirectory()); + } + $this->baseDir = $baseDir; + $this->appDir = $baseDir . '/application'; + $this->vendorDir = $baseDir . '/library/vendor'; $this->libDir = realpath(__DIR__ . '/../..'); - if (!defined('ICINGA_LIBDIR')) { - define('ICINGA_LIBDIR', $this->libDir); - } - - if (defined('ICINGAWEB_APPDIR')) { - $this->appDir = ICINGAWEB_APPDIR; - } elseif (array_key_exists('ICINGAWEB_APPDIR', $_SERVER)) { - $this->appDir = $_SERVER['ICINGAWEB_APPDIR']; - } else { - $this->appDir = realpath($this->libDir. '/../application'); - } - - if (!defined('ICINGAWEB_APPDIR')) { - define('ICINGAWEB_APPDIR', $this->appDir); - } - if ($configDir === null) { if (array_key_exists('ICINGAWEB_CONFIGDIR', $_SERVER)) { $configDir = $_SERVER['ICINGAWEB_CONFIGDIR']; @@ -196,44 +205,6 @@ abstract class ApplicationBootstrap return $this->isWeb; } - /** - * Getter for application dir - * - * Optional append sub directory - * - * @param string $subdir optional subdir - * - * @return string - */ - public function getApplicationDir($subdir = null) - { - return $this->getDirWithSubDir($this->appDir, $subdir); - } - - /** - * Getter for config dir - * - * @param string $subdir - * - * @return string - */ - public function getConfigDir($subdir = null) - { - return $this->getDirWithSubDir($this->configDir, $subdir); - } - - /** - * Get the path to the bootstrapping directory. - * - * This is usually /public for Web and EmbeddedWeb - * - * @return string - */ - public function getBootstrapDirecory() - { - return dirname($_SERVER['SCRIPT_FILENAME']); - } - /** * Helper to glue directories together * @@ -252,15 +223,76 @@ abstract class ApplicationBootstrap } /** - * Starting concrete bootstrap classes + * Get the base directory * - * @param string $configDir + * @param string $subDir Optional sub directory to get * - * @return ApplicationBootstrap + * @return string */ - public static function start($configDir = null) + public function getBaseDir($subDir = null) { - $application = new static($configDir); + return $this->getDirWithSubDir($this->baseDir, $subDir); + } + + /** + * Get the application directory + * + * @param string $subDir Optional sub directory to get + * + * @return string + */ + public function getApplicationDir($subDir = null) + { + return $this->getDirWithSubDir($this->appDir, $subDir); + } + + /** + * Get the vendor library directory + * + * @param string $subDir Optional sub directory to get + * + * @return string + */ + public function getVendorDir($subDir = null) + { + return $this->getDirWithSubDir($this->vendorDir, $subDir); + } + + /** + * Get the configuration directory + * + * @param string $subDir Optional sub directory to get + * + * @return string + */ + public function getConfigDir($subDir = null) + { + return $this->getDirWithSubDir($this->configDir, $subDir); + } + + /** + * Get the path to the bootstrapping directory + * + * This is usually /public for Web and EmbeddedWeb and /bin for the CLI + * + * @return string + */ + public function getBootstrapDirectory() + { + return dirname(realpath($_SERVER['SCRIPT_FILENAME'])); + } + + /** + * Start the bootstrap + * + * @param string $baseDir Icinga Web 2 base directory + * @param string $configDir Path to Icinga Web 2's configuration files + * + * @return static + */ + public static function start($baseDir = null, $configDir = null) + { + $application = new static($baseDir, $configDir); $application->bootstrap(); return $application; } @@ -313,16 +345,26 @@ abstract class ApplicationBootstrap $this->moduleManager = new ModuleManager( $this, $this->configDir . '/enabledModules', - explode( - ':', - $this->config->global !== null - ? $this->config->global->get('modulePath', ICINGAWEB_APPDIR . '/../modules') - : ICINGAWEB_APPDIR . '/../modules' - ) + explode(':', $this->config->fromSection('global', 'modulePath', $this->baseDir . '/modules')) ); return $this; } + /** + * Load all core modules + * + * @return self + */ + protected function loadCoreModules() + { + try { + $this->moduleManager->loadCoreModules(); + } catch (NotReadableError $e) { + Logger::error(new IcingaException('Cannot load core modules. An exception was thrown:', $e)); + } + return $this; + } + /** * Load all enabled modules * @@ -346,7 +388,7 @@ abstract class ApplicationBootstrap protected function setupLogging() { Logger::create( - new Zend_Config( + new Config( array( 'log' => 'syslog' ) @@ -363,12 +405,14 @@ abstract class ApplicationBootstrap protected function loadConfig() { Config::$configDir = $this->configDir; + try { $this->config = Config::app(); } catch (NotReadableError $e) { Logger::error(new IcingaException('Cannot load application configuration. An exception was thrown:', $e)); - $this->config = new Zend_Config(array()); + $this->config = new Config(); } + return $this; } @@ -388,6 +432,7 @@ abstract class ApplicationBootstrap return false; // Continue with the normal error handler } switch($errno) { + case E_WARNING: case E_STRICT: throw new ErrorException($errstr, 0, $errno, $errfile, $errline); } @@ -403,9 +448,9 @@ abstract class ApplicationBootstrap */ protected function setupLogger() { - if ($this->config->logging !== null) { + if (($loggingConfig = $this->config->logging) !== null) { try { - Logger::create($this->config->logging); + Logger::create($loggingConfig); } catch (ConfigurationError $e) { Logger::error($e); } @@ -444,7 +489,7 @@ abstract class ApplicationBootstrap if (! $default) { $default = 'UTC'; } - $timeZoneString = $this->config->global !== null ? $this->config->global->get('timezone', $default) : $default; + $timeZoneString = $this->config->fromSection('global', 'timezone', $default); date_default_timezone_set($timeZoneString); DateTimeFactory::setConfig(array('timezone' => $timeZoneString)); return $this; diff --git a/library/Icinga/Application/Cli.php b/library/Icinga/Application/Cli.php index d329ce439..9f14b6c20 100644 --- a/library/Icinga/Application/Cli.php +++ b/library/Icinga/Application/Cli.php @@ -4,15 +4,15 @@ namespace Icinga\Application; +use Icinga\Application\Config; use Icinga\Application\Platform; use Icinga\Application\ApplicationBootstrap; use Icinga\Cli\Params; use Icinga\Cli\Loader; use Icinga\Cli\Screen; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Application\Benchmark; use Icinga\Exception\ProgrammingError; -use Zend_Config; require_once __DIR__ . '/ApplicationBootstrap.php'; @@ -43,17 +43,17 @@ class Cli extends ApplicationBootstrap ->parseBasicParams() ->setupLogger() ->setupResourceFactory() - ->setupModuleManager(); + ->setupModuleManager() + ->loadCoreModules(); } protected function setupLogging() { Logger::create( - new Zend_Config( + new Config( array( 'level' => Logger::INFO, - 'log' => 'file', - 'file' => 'php://stderr' + 'log' => 'stdout', ) ) ); diff --git a/library/Icinga/Application/Config.php b/library/Icinga/Application/Config.php index c7998a0bb..6a08eb93e 100644 --- a/library/Icinga/Application/Config.php +++ b/library/Icinga/Application/Config.php @@ -4,14 +4,17 @@ namespace Icinga\Application; -use Zend_Config; -use Zend_Config_Ini; +use Iterator; +use Countable; +use ArrayAccess; +use LogicException; +use UnexpectedValueException; use Icinga\Exception\NotReadableError; /** - * Global registry of application and module configuration. + * Container for configuration values and global registry of application and module related configuration. */ -class Config extends Zend_Config +class Config implements Countable, Iterator, ArrayAccess { /** * Configuration directory where ALL (application and module) configuration is located @@ -20,13 +23,6 @@ class Config extends Zend_Config */ public static $configDir; - /** - * The INI file this configuration has been loaded from or should be written to - * - * @var string - */ - protected $configFile; - /** * Application config instances per file * @@ -42,95 +38,34 @@ class Config extends Zend_Config protected static $modules = array(); /** - * Load configuration from the given INI file + * This config's data * - * @param string $file The file to parse - * - * @throws NotReadableError When the file does not exist or cannot be read + * @var array */ - public static function fromIni($file) - { - $config = new static(array(), true); - $filepath = realpath($file); - - if ($filepath === false) { - $config->setConfigFile($file); - } elseif (is_readable($filepath)) { - $config->setConfigFile($filepath); - $config->merge(new Zend_Config_Ini($filepath)); - } else { - throw new NotReadableError('Cannot read config file "%s". Permission denied', $filepath); - } - - return $config; - } + protected $data; /** - * Retrieve a application config instance + * The INI file this configuration has been loaded from or should be written to * - * @param string $configname The configuration name (without ini suffix) to read and return - * @param bool $fromDisk When set true, the configuration will be read from the disk, even - * if it already has been read - * - * @return Config The configuration object that has been requested + * @var string */ - public static function app($configname = 'config', $fromDisk = false) - { - if (!isset(self::$app[$configname]) || $fromDisk) { - self::$app[$configname] = Config::fromIni(self::resolvePath($configname . '.ini')); - } - return self::$app[$configname]; - } + protected $configFile; /** - * Set module config + * Create a new config * - * @param string $moduleName - * @param string $configName - * @param Zend_Config $config + * @param array $data The data to initialize the new config with */ - public static function setModuleConfig($moduleName, $configName, Zend_Config $config) + public function __construct(array $data = array()) { - self::$modules[$moduleName][$configName] = $config; - } + $this->data = array(); - /** - * Retrieve a module config instance - * - * @param string $modulename The name of the module to look for configurations - * @param string $configname The configuration name (without ini suffix) to read and return - * @param string $fromDisk Whether to read the configuration from disk - * - * @return Config The configuration object that has been requested - */ - public static function module($modulename, $configname = 'config', $fromDisk = false) - { - if (!isset(self::$modules[$modulename])) { - self::$modules[$modulename] = array(); - } - $moduleConfigs = self::$modules[$modulename]; - if (!isset($moduleConfigs[$configname]) || $fromDisk) { - $moduleConfigs[$configname] = Config::fromIni( - self::resolvePath('modules/' . $modulename . '/' . $configname . '.ini') - ); - } - return $moduleConfigs[$configname]; - } - - /** - * Retrieve names of accessible sections or properties - * - * @param $name - * @return array - */ - public function keys($name = null) - { - if ($name === null) { - return array_keys($this->toArray()); - } elseif ($this->$name === null) { - return array(); - } else { - return array_keys($this->$name->toArray()); + foreach ($data as $key => $value) { + if (is_array($value)) { + $this->data[$key] = new static($value); + } else { + $this->data[$key] = $value; + } } } @@ -158,13 +93,375 @@ class Config extends Zend_Config } /** - * Prepend configuration base dir if input is relative + * Deep clone this config + */ + public function __clone() + { + $array = array(); + foreach ($this->data as $key => $value) { + if ($value instanceof self) { + $array[$key] = clone $value; + } else { + $array[$key] = $value; + } + } + + $this->data = $array; + } + + /** + * Return the count of available sections and properties * - * @param string $path Input path - * @return string Absolute path + * @return int + */ + public function count() + { + return count($this->data); + } + + /** + * Reset the current position of $this->data + * + * @return mixed + */ + public function rewind() + { + return reset($this->data); + } + + /** + * Return the section's or property's value of the current iteration + * + * @return mixed + */ + public function current() + { + return current($this->data); + } + + /** + * Return whether the position of the current iteration is valid + * + * @return bool + */ + public function valid() + { + return key($this->data) !== null; + } + + /** + * Return the section's or property's name of the current iteration + * + * @return mixed + */ + public function key() + { + return key($this->data); + } + + /** + * Advance the position of the current iteration and return the new section's or property's value + * + * @return mixed + */ + public function next() + { + return next($this->data); + } + + /** + * Return whether the given section or property is set + * + * @param string $key The name of the section or property + * + * @return bool + */ + public function __isset($key) + { + return isset($this->data[$key]); + } + + /** + * Return the value for the given property or the config for the given section + * + * @param string $key The name of the property or section + * + * @return mixed|NULL The value or NULL in case $key does not exist + */ + public function __get($key) + { + if (array_key_exists($key, $this->data)) { + return $this->data[$key]; + } + } + + /** + * Add a new property or section + * + * @param string $key The name of the new property or section + * @param mixed $value The value to set for the new property or section + */ + public function __set($key, $value) + { + if (is_array($value)) { + $this->data[$key] = new static($value); + } else { + $this->data[$key] = $value; + } + } + + /** + * Remove the given property or section + * + * @param string $key The property or section to remove + */ + public function __unset($key) + { + unset($this->data[$key]); + } + + /** + * Return whether the given section or property is set + * + * @param string $key The name of the section or property + * + * @return bool + */ + public function offsetExists($key) + { + return isset($this->$key); + } + + /** + * Return the value for the given property or the config for the given section + * + * @param string $key The name of the property or section + * + * @return mixed|NULL The value or NULL in case $key does not exist + */ + public function offsetGet($key) + { + return $this->$key; + } + + /** + * Add a new property or section + * + * @param string $key The name of the new property or section + * @param mixed $value The value to set for the new property or section + */ + public function offsetSet($key, $value) + { + if ($key === null) { + throw new LogicException('Appending values without an explicit key is not supported'); + } + + $this->$key = $value; + } + + /** + * Remove the given property or section + * + * @param string $key The property or section to remove + */ + public function offsetUnset($key) + { + unset($this->$key); + } + + /** + * Return whether this config has any data + * + * @return bool + */ + public function isEmpty() + { + return $this->count() === 0; + } + + /** + * Return the value for the given property or the config for the given section + * + * @param string $key The name of the property or section + * @param mixed $default The value to return in case the property or section is missing + * + * @return mixed + */ + public function get($key, $default = null) + { + $value = $this->$key; + if ($default !== null && $value === null) { + $value = $default; + } + + return $value; + } + + /** + * Return all section and property names + * + * @return array + */ + public function keys() + { + return array_keys($this->data); + } + + /** + * Return this config's data as associative array + * + * @return array + */ + public function toArray() + { + $array = array(); + foreach ($this->data as $key => $value) { + if ($value instanceof self) { + $array[$key] = $value->toArray(); + } else { + $array[$key] = $value; + } + } + + return $array; + } + + /** + * Merge the given data with this config + * + * @param array|Config $data An array or a config + * + * @return self + */ + public function merge($data) + { + if ($data instanceof self) { + $data = $data->toArray(); + } + + foreach ($data as $key => $value) { + if (array_key_exists($key, $this->data)) { + if (is_array($value)) { + if ($this->data[$key] instanceof self) { + $this->data[$key]->merge($value); + } else { + $this->data[$key] = new static($value); + } + } else { + $this->data[$key] = $value; + } + } else { + $this->data[$key] = is_array($value) ? new static($value) : $value; + } + } + } + + /** + * Return the value from a section's property + * + * @param string $section The section where the given property can be found + * @param string $key The section's property to fetch the value from + * @param mixed $default The value to return in case the section or the property is missing + * + * @return mixed + * + * @throws UnexpectedValueException In case the given section does not hold any configuration + */ + public function fromSection($section, $key, $default = null) + { + $value = $this->$section; + if ($value instanceof self) { + $value = $value->$key; + } elseif ($value !== null) { + throw new UnexpectedValueException( + sprintf('Value "%s" is not of type "Config" or a sub-type of it', $value) + ); + } + + if ($value === null && $default !== null) { + $value = $default; + } + + return $value; + } + + /** + * Load configuration from the given INI file + * + * @param string $file The file to parse + * + * @throws NotReadableError When the file does not exist or cannot be read + */ + public static function fromIni($file) + { + $config = new static(); + + $filepath = realpath($file); + if ($filepath === false) { + $config->setConfigFile($file); + } elseif (is_readable($filepath)) { + $config->setConfigFile($filepath); + $config->merge(parse_ini_file($filepath, true)); + } else { + throw new NotReadableError(t('Cannot read config file "%s". Permission denied'), $filepath); + } + + return $config; + } + + /** + * Prepend configuration base dir to the given relative path + * + * @param string $path A relative path + * + * @return string */ public static function resolvePath($path) { return self::$configDir . DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR); } + + /** + * Retrieve a application config + * + * @param string $configname The configuration name (without ini suffix) to read and return + * @param bool $fromDisk When set true, the configuration will be read from disk, even + * if it already has been read + * + * @return Config The requested configuration + */ + public static function app($configname = 'config', $fromDisk = false) + { + if (!isset(self::$app[$configname]) || $fromDisk) { + self::$app[$configname] = static::fromIni(static::resolvePath($configname . '.ini')); + } + + return self::$app[$configname]; + } + + /** + * Retrieve a module config + * + * @param string $modulename The name of the module where to look for the requested configuration + * @param string $configname The configuration name (without ini suffix) to read and return + * @param string $fromDisk When set true, the configuration will be read from disk, even + * if it already has been read + * + * @return Config The requested configuration + */ + public static function module($modulename, $configname = 'config', $fromDisk = false) + { + if (!isset(self::$modules[$modulename])) { + self::$modules[$modulename] = array(); + } + + $moduleConfigs = self::$modules[$modulename]; + if (!isset($moduleConfigs[$configname]) || $fromDisk) { + $moduleConfigs[$configname] = static::fromIni( + static::resolvePath('modules/' . $modulename . '/' . $configname . '.ini') + ); + } + + return $moduleConfigs[$configname]; + } } diff --git a/library/Icinga/Application/EmbeddedWeb.php b/library/Icinga/Application/EmbeddedWeb.php index 6a160017c..0dd8b5d5c 100644 --- a/library/Icinga/Application/EmbeddedWeb.php +++ b/library/Icinga/Application/EmbeddedWeb.php @@ -31,6 +31,7 @@ class EmbeddedWeb extends ApplicationBootstrap ->setupErrorHandling() ->setupTimezone() ->setupModuleManager() + ->loadCoreModules() ->loadEnabledModules(); } } diff --git a/library/Icinga/Logger/Logger.php b/library/Icinga/Application/Logger.php similarity index 89% rename from library/Icinga/Logger/Logger.php rename to library/Icinga/Application/Logger.php index a5458e498..f74c6d021 100644 --- a/library/Icinga/Logger/Logger.php +++ b/library/Icinga/Application/Logger.php @@ -2,13 +2,13 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -namespace Icinga\Logger; +namespace Icinga\Application; use Exception; -use Zend_Config; +use Icinga\Application\Config; +use Icinga\Application\Logger\Writer\FileWriter; +use Icinga\Application\Logger\Writer\SyslogWriter; use Icinga\Exception\ConfigurationError; -use Icinga\Logger\Writer\FileWriter; -use Icinga\Logger\Writer\SyslogWriter; /** * Logger @@ -57,7 +57,7 @@ class Logger /** * Log writer * - * @var \Icinga\Logger\LogWriter + * @var \Icinga\Application\Logger\LogWriter */ protected $writer; @@ -71,12 +71,12 @@ class Logger /** * Create a new logger object * - * @param Zend_Config $config + * @param Config $config * * @throws ConfigurationError If the logging configuration directive 'log' is missing or if the logging level is * not defined */ - public function __construct(Zend_Config $config) + public function __construct(Config $config) { if ($config->log === null) { throw new ConfigurationError('Required logging configuration directive \'log\' missing'); @@ -118,11 +118,11 @@ class Logger /** * Create a new logger object * - * @param Zend_Config $config + * @param Config $config * * @return static */ - public static function create(Zend_Config $config) + public static function create(Config $config) { static::$instance = new static($config); return static::$instance; @@ -131,14 +131,14 @@ class Logger /** * Create a log writer * - * @param Zend_Config $config The configuration to initialize the writer with + * @param Config $config The configuration to initialize the writer with * - * @return \Icinga\Logger\LogWriter The requested log writer - * @throws ConfigurationError If the requested writer cannot be found + * @return \Icinga\Application\Logger\LogWriter The requested log writer + * @throws ConfigurationError If the requested writer cannot be found */ - protected function createWriter(Zend_Config $config) + protected function createWriter(Config $config) { - $class = 'Icinga\\Logger\\Writer\\' . ucfirst(strtolower($config->log)) . 'Writer'; + $class = 'Icinga\\Application\\Logger\\Writer\\' . ucfirst(strtolower($config->log)) . 'Writer'; if (! class_exists($class)) { throw new ConfigurationError( 'Cannot find log writer of type "%s"', @@ -258,7 +258,7 @@ class Logger /** * Get the log writer to use * - * @return \Icinga\Logger\LogWriter + * @return \Icinga\Application\Logger\LogWriter */ public function getWriter() { diff --git a/library/Icinga/Logger/LogWriter.php b/library/Icinga/Application/Logger/LogWriter.php similarity index 62% rename from library/Icinga/Logger/LogWriter.php rename to library/Icinga/Application/Logger/LogWriter.php index 3222e50be..831c96fe8 100644 --- a/library/Icinga/Logger/LogWriter.php +++ b/library/Icinga/Application/Logger/LogWriter.php @@ -2,19 +2,27 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -namespace Icinga\Logger; +namespace Icinga\Application\Logger; -use Zend_Config; +use Icinga\Application\Config; /** * Abstract class for writers that write messages to a log */ abstract class LogWriter { + /** + * @var Zend_Config + */ + protected $config; + /** * Create a new log writer initialized with the given configuration */ - abstract public function __construct(Zend_Config $config); + public function __construct(Config $config) + { + $this->config = $config; + } /** * Log a message with the given severity diff --git a/library/Icinga/Logger/Writer/FileWriter.php b/library/Icinga/Application/Logger/Writer/FileWriter.php similarity index 89% rename from library/Icinga/Logger/Writer/FileWriter.php rename to library/Icinga/Application/Logger/Writer/FileWriter.php index 527358e1c..eec3b038f 100644 --- a/library/Icinga/Logger/Writer/FileWriter.php +++ b/library/Icinga/Application/Logger/Writer/FileWriter.php @@ -2,13 +2,13 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -namespace Icinga\Logger\Writer; +namespace Icinga\Application\Logger\Writer; use Exception; -use Zend_Config; +use Icinga\Application\Config; +use Icinga\Application\Logger; +use Icinga\Application\Logger\LogWriter; use Icinga\Exception\ConfigurationError; -use Icinga\Logger\Logger; -use Icinga\Logger\LogWriter; use Icinga\Util\File; /** @@ -26,12 +26,12 @@ class FileWriter extends LogWriter /** * Create a new file log writer * - * @param Zend_Config $config + * @param Config $config * * @throws ConfigurationError If the configuration directive 'file' is missing or if the path to 'file' does * not exist or if writing to 'file' is not possible */ - public function __construct(Zend_Config $config) + public function __construct(Config $config) { if ($config->file === null) { throw new ConfigurationError('Required logging configuration directive \'file\' missing'); diff --git a/library/Icinga/Application/Logger/Writer/StdoutWriter.php b/library/Icinga/Application/Logger/Writer/StdoutWriter.php new file mode 100644 index 000000000..1fe929d92 --- /dev/null +++ b/library/Icinga/Application/Logger/Writer/StdoutWriter.php @@ -0,0 +1,50 @@ +screen === null) { + $this->screen = Screen::instance(); + } + return $this->screen; + } + + /** + * Log a message with the given severity + * + * @param int $severity The severity to use + * @param string $message The message to log + */ + public function log($severity, $message) + { + $color = 'black'; + switch ($severity) { + case Logger::$ERROR: + $color = 'red'; + break; + case Logger::$WARNING: + $color = 'orange'; + break; + case Logger::$INFO: + $color = 'green'; + break; + case Logger::$DEBUG: + $color = 'blue'; + break; + } + file_put_contents('php://stderr', $this->screen()->colorize($message, $color) . "\n"); + } +} diff --git a/library/Icinga/Logger/Writer/SyslogWriter.php b/library/Icinga/Application/Logger/Writer/SyslogWriter.php similarity index 84% rename from library/Icinga/Logger/Writer/SyslogWriter.php rename to library/Icinga/Application/Logger/Writer/SyslogWriter.php index f67c618d2..6c39eee28 100644 --- a/library/Icinga/Logger/Writer/SyslogWriter.php +++ b/library/Icinga/Application/Logger/Writer/SyslogWriter.php @@ -2,11 +2,11 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -namespace Icinga\Logger\Writer; +namespace Icinga\Application\Logger\Writer; -use Zend_Config; -use Icinga\Logger\Logger; -use Icinga\Logger\LogWriter; +use Icinga\Application\Config; +use Icinga\Application\Logger; +use Icinga\Application\Logger\LogWriter; /** * Log to the syslog service @@ -51,9 +51,9 @@ class SyslogWriter extends LogWriter /** * Create a new syslog log writer * - * @param Zend_Config $config + * @param Config $config */ - public function __construct(Zend_Config $config) + public function __construct(Config $config) { $this->ident = $config->get('application', 'icingaweb'); $this->facility = static::$facilities['user']; diff --git a/library/Icinga/Application/Modules/Manager.php b/library/Icinga/Application/Modules/Manager.php index 60c2ee85b..8857bf2d2 100644 --- a/library/Icinga/Application/Modules/Manager.php +++ b/library/Icinga/Application/Modules/Manager.php @@ -6,7 +6,7 @@ namespace Icinga\Application\Modules; use Icinga\Application\ApplicationBootstrap; use Icinga\Application\Icinga; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Data\DataArray\ArrayDatasource; use Icinga\Data\SimpleQuery; use Icinga\Exception\ConfigurationError; @@ -67,6 +67,18 @@ class Manager */ private $modulePaths = array(); + /** + * The core modules + * + * Core modules do not need to be enabled to load and cannot be disabled + * by the user. This must not be writable programmatically! + * + * @var array + */ + private $coreModules = array( + 'setup' + ); + /** * Create a new instance of the module manager * @@ -157,7 +169,21 @@ class Manager } /** - * Try to set all enabled modules in loaded sate + * Try to set all core modules in loaded state + * + * @return self + * @see Manager::loadModule() + */ + public function loadCoreModules() + { + foreach ($this->coreModules as $name) { + $this->loadModule($name); + } + return $this; + } + + /** + * Try to set all enabled modules in loaded state * * @return self * @see Manager::loadModule() @@ -211,6 +237,8 @@ class Manager 'Cannot enable module "%s". Module is not installed.', $name ); + } elseif (in_array($name, $this->coreModules)) { + return $this; } clearstatcache(true); @@ -427,7 +455,7 @@ class Manager } $installed = $this->listInstalledModules(); - foreach ($installed as $name) { + foreach (array_diff($installed, $this->coreModules) as $name) { $info[$name] = (object) array( 'name' => $name, 'path' => $this->installedBaseDirs[$name], @@ -487,11 +515,14 @@ class Manager /** * Detect installed modules from every path provided in modulePaths * + * @param array $availableDirs Installed modules location + * * @return self */ - public function detectInstalledModules() + public function detectInstalledModules(array $availableDirs = null) { - foreach ($this->modulePaths as $basedir) { + $modulePaths = $availableDirs !== null ? $availableDirs : $this->modulePaths; + foreach ($modulePaths as $basedir) { $canonical = realpath($basedir); if ($canonical === false) { Logger::warning('Module path "%s" does not exist', $basedir); @@ -528,4 +559,14 @@ class Manager ksort($this->installedBaseDirs); return $this; } + + /** + * Get the directories where to look for installed modules + * + * @return array + */ + public function getModuleDirs() + { + return $this->modulePaths; + } } diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index b1b051a6c..461cec59a 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -5,19 +5,19 @@ namespace Icinga\Application\Modules; use Exception; -use Zend_Config; use Zend_Controller_Router_Route_Abstract; use Zend_Controller_Router_Route as Route; use Zend_Controller_Router_Route_Regex as RegexRoute; use Icinga\Application\ApplicationBootstrap; use Icinga\Application\Config; use Icinga\Application\Icinga; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Util\Translator; use Icinga\Web\Hook; use Icinga\Web\Menu; use Icinga\Web\Widget; use Icinga\Web\Widget\Dashboard\Pane; +use Icinga\Module\Setup\SetupWizard; use Icinga\Util\File; use Icinga\Exception\ProgrammingError; use Icinga\Exception\IcingaException; @@ -134,6 +134,13 @@ class Module */ private $configTabs = array(); + /** + * Provided setup wizard + * + * @var string + */ + private $setupWizard; + /** * Icinga application * @@ -235,7 +242,7 @@ class Module if (array_key_exists($name, $this->menuItems)) { $this->menuItems[$name]->setProperties($properties); } else { - $this->menuItems[$name] = new Menu($name, new Zend_Config($properties)); + $this->menuItems[$name] = new Menu($name, new Config($properties)); } return $this->menuItems[$name]; @@ -642,6 +649,31 @@ class Module return $tabs; } + /** + * Whether this module provides a setup wizard + * + * @return bool + */ + public function providesSetupWizard() + { + $this->launchConfigScript(); + if (class_exists($this->setupWizard)) { + $wizard = new $this->setupWizard; + return $wizard instanceof SetupWizard; + } + + return false; + } + + /** + * Return this module's setup wizard + * + * @return SetupWizard + */ + public function getSetupWizard() + { + return new $this->setupWizard; + } /** * Provide a named permission @@ -705,6 +737,19 @@ class Module return $this; } + /** + * Provide a setup wizard + * + * @param string $className The name of the class + * + * @return self + */ + protected function provideSetupWizard($className) + { + $this->setupWizard = $className; + return $this; + } + /** * Register new namespaces on the autoloader * diff --git a/library/Icinga/Application/Platform.php b/library/Icinga/Application/Platform.php index 1ca738741..259219435 100644 --- a/library/Icinga/Application/Platform.php +++ b/library/Icinga/Application/Platform.php @@ -30,6 +30,16 @@ class Platform */ protected static $fqdn; + /** + * Return the operating system's name + * + * @return string + */ + public static function getOperatingSystemName() + { + return php_uname('s'); + } + /** * Test of windows * @@ -37,7 +47,7 @@ class Platform */ public static function isWindows() { - return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; + return strtoupper(substr(self::getOperatingSystemName(), 0, 3)) === 'WIN'; } /** @@ -47,7 +57,7 @@ class Platform */ public static function isLinux() { - return strtoupper(substr(PHP_OS, 0, 5)) === 'LINUX'; + return strtoupper(substr(self::getOperatingSystemName(), 0, 5)) === 'LINUX'; } /** @@ -116,7 +126,35 @@ class Platform if (substr(self::$fqdn, 0, strlen(self::$hostname)) === self::$hostname) { self::$domain = substr(self::$fqdn, strlen(self::$hostname) + 1); } else { - self::$domain = array_shift(preg_split('~\.~', self::$hostname, 2)); + $parts = preg_split('~\.~', self::$hostname, 2); + self::$domain = array_shift($parts); + } + } + + /** + * Return the version of PHP + * + * @return string + */ + public static function getPhpVersion() + { + return phpversion(); + } + + /** + * Return the username PHP is running as + * + * @return string + */ + public static function getPhpUser() + { + if (static::isWindows()) { + return get_current_user(); // http://php.net/manual/en/function.get-current-user.php#75059 + } + + if (function_exists('posix_geteuid')) { + $userInfo = posix_getpwuid(posix_geteuid()); + return $userInfo['name']; } } @@ -131,4 +169,32 @@ class Platform { return extension_loaded($extensionName); } + + /** + * Return the value for the given PHP configuration option + * + * @param string $option The option name for which to return the value + * + * @return string|false + */ + public static function getPhpConfig($option) + { + return ini_get($option); + } + + /** + * Return whether the given Zend framework class exists + * + * @param string $name The name of the class to check + * + * @return bool + */ + public static function zendClassExists($name) + { + if (class_exists($name)) { + return true; + } + + return (@include str_replace('_', '/', $name) . '.php') !== false; + } } diff --git a/library/Icinga/Application/Web.php b/library/Icinga/Application/Web.php index 89cad22ab..23b0634a0 100644 --- a/library/Icinga/Application/Web.php +++ b/library/Icinga/Application/Web.php @@ -9,7 +9,7 @@ require_once __DIR__ . '/ApplicationBootstrap.php'; use Icinga\Authentication\Manager as AuthenticationManager; use Icinga\Exception\ConfigurationError; use Icinga\Exception\NotReadableError; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Util\TimezoneDetect; use Icinga\Web\Request; use Icinga\Web\Response; @@ -92,6 +92,7 @@ class Web extends ApplicationBootstrap ->setupZendMvc() ->setupFormNamespace() ->setupModuleManager() + ->loadCoreModules() ->loadEnabledModules() ->setupRoute() ->setupPagination(); diff --git a/library/Icinga/Authentication/AuthChain.php b/library/Icinga/Authentication/AuthChain.php index 4ab87f7ae..d704f2932 100644 --- a/library/Icinga/Authentication/AuthChain.php +++ b/library/Icinga/Authentication/AuthChain.php @@ -5,8 +5,8 @@ namespace Icinga\Authentication; use Iterator; -use Zend_Config; -use Icinga\Logger\Logger; +use Icinga\Application\Config; +use Icinga\Application\Logger; use Icinga\Exception\ConfigurationError; /** @@ -17,7 +17,7 @@ class AuthChain implements Iterator /** * User backends configuration * - * @var Zend_Config + * @var Config */ private $config; @@ -31,9 +31,9 @@ class AuthChain implements Iterator /** * Create a new authentication chain from config * - * @param Zend_Config $config User backends configuration + * @param Config $config User backends configuration */ - public function __construct(Zend_Config $config) + public function __construct(Config $config) { $this->config = $config; } diff --git a/library/Icinga/Authentication/Backend/AutoLoginBackend.php b/library/Icinga/Authentication/Backend/AutoLoginBackend.php index 16373bb6c..990797088 100644 --- a/library/Icinga/Authentication/Backend/AutoLoginBackend.php +++ b/library/Icinga/Authentication/Backend/AutoLoginBackend.php @@ -4,7 +4,7 @@ namespace Icinga\Authentication\Backend; -use Zend_Config; +use Icinga\Application\Config; use Icinga\Authentication\UserBackend; use Icinga\User; @@ -23,9 +23,9 @@ class AutoLoginBackend extends UserBackend /** * Create new autologin backend * - * @param Zend_Config $config + * @param Config $config */ - public function __construct(Zend_Config $config) + public function __construct(Config $config) { $this->stripUsernameRegexp = $config->get('strip_username_regexp'); } @@ -54,7 +54,7 @@ class AutoLoginBackend extends UserBackend if (isset($_SERVER['REMOTE_USER'])) { $username = $_SERVER['REMOTE_USER']; $user->setRemoteUserInformation($username, 'REMOTE_USER'); - if ($this->stripUsernameRegexp !== null) { + if ($this->stripUsernameRegexp) { $stripped = preg_replace($this->stripUsernameRegexp, '', $username); if ($stripped !== false) { // TODO(el): PHP issues a warning when PHP cannot compile the regular expression. Should we log an diff --git a/library/Icinga/Authentication/Backend/DbUserBackend.php b/library/Icinga/Authentication/Backend/DbUserBackend.php index a89a07c81..d2d0147e5 100644 --- a/library/Icinga/Authentication/Backend/DbUserBackend.php +++ b/library/Icinga/Authentication/Backend/DbUserBackend.php @@ -4,6 +4,7 @@ namespace Icinga\Authentication\Backend; +use PDO; use Icinga\Authentication\UserBackend; use Icinga\Data\Db\DbConnection; use Icinga\User; @@ -11,16 +12,29 @@ use Icinga\Exception\AuthenticationException; use Exception; use Zend_Db_Expr; use Zend_Db_Select; -use Icinga\Exception\IcingaException; class DbUserBackend extends UserBackend { + /** + * The algorithm to use when hashing passwords + * + * @var string + */ + const HASH_ALGORITHM = '$1$'; // MD5 + + /** + * The length of the salt to use when hashing a password + * + * @var int + */ + const SALT_LENGTH = 12; // 12 is required by MD5 + /** * Connection to the database * * @var DbConnection */ - private $conn; + protected $conn; public function __construct(DbConnection $conn) { @@ -36,45 +50,69 @@ class DbUserBackend extends UserBackend */ public function hasUser(User $user) { - $select = new Zend_Db_Select($this->conn->getConnection()); - $row = $select->from('account', array(new Zend_Db_Expr(1))) - ->where('username = ?', $user->getUsername()) + $select = new Zend_Db_Select($this->conn->getDbAdapter()); + $row = $select->from('icingaweb_user', array(new Zend_Db_Expr(1))) + ->where('name = ?', $user->getUsername()) ->query()->fetchObject(); return ($row !== false) ? true : false; } /** - * Authenticate the given user and return true on success, false on failure and null on error + * Add a new user + * + * @param string $username The name of the new user + * @param string $password The new user's password + * @param bool $active Whether the user is active + */ + public function addUser($username, $password, $active = true) + { + $passwordHash = $this->hashPassword($password); + + $stmt = $this->conn->getDbAdapter()->prepare( + 'INSERT INTO icingaweb_user VALUES (:name, :active, :password_hash, now(), DEFAULT);' + ); + $stmt->bindParam(':name', $username, PDO::PARAM_STR); + $stmt->bindParam(':active', $active, PDO::PARAM_INT); + $stmt->bindParam(':password_hash', $passwordHash, PDO::PARAM_LOB); + $stmt->execute(); + } + + /** + * Fetch the hashed password for the given user + * + * @param string $username The name of the user + * + * @return string + */ + protected function getPasswordHash($username) + { + $stmt = $this->conn->getDbAdapter()->prepare( + 'SELECT password_hash FROM icingaweb_user WHERE name = :name AND active = 1' + ); + $stmt->execute(array(':name' => $username)); + $stmt->bindColumn(1, $lob, PDO::PARAM_LOB); + $stmt->fetch(PDO::FETCH_BOUND); + return is_resource($lob) ? stream_get_contents($lob) : $lob; + } + + /** + * Authenticate the given user and return true on success, false on failure and throw an exception on error * * @param User $user * @param string $password * - * @return bool|null + * @return bool + * * @throws AuthenticationException */ public function authenticate(User $user, $password) { try { - $salt = $this->getSalt($user->getUsername()); - if ($salt === null) { - return false; - } - if ($salt === '') { - throw new IcingaException( - 'Cannot find salt for user %s', - $user->getUsername() - ); - } - - $select = new Zend_Db_Select($this->conn->getConnection()); - $row = $select->from('account', array(new Zend_Db_Expr(1))) - ->where('username = ?', $user->getUsername()) - ->where('active = ?', true) - ->where('password = ?', $this->hashPassword($password, $salt)) - ->query()->fetchObject(); - - return ($row !== false) ? true : false; + $passwordHash = $this->getPasswordHash($user->getUsername()); + $passwordSalt = $this->getSalt($passwordHash); + $hashToCompare = $this->hashPassword($password, $passwordSalt); + return $hashToCompare === $passwordHash; } catch (Exception $e) { throw new AuthenticationException( 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:', @@ -86,29 +124,40 @@ class DbUserBackend extends UserBackend } /** - * Get salt by username + * Extract salt from the given password hash * - * @param string $username + * @param string $hash The hashed password * - * @return string|null + * @return string */ - private function getSalt($username) + protected function getSalt($hash) { - $select = new Zend_Db_Select($this->conn->getConnection()); - $row = $select->from('account', array('salt'))->where('username = ?', $username)->query()->fetchObject(); - return ($row !== false) ? $row->salt : null; + return substr($hash, strlen(self::HASH_ALGORITHM), self::SALT_LENGTH); + } + + /** + * Return a random salt + * + * The returned salt is safe to be used for hashing a user's password + * + * @return string + */ + protected function generateSalt() + { + return openssl_random_pseudo_bytes(self::SALT_LENGTH); } /** * Hash a password * - * @param string $password - * @param string $salt + * @param string $password + * @param string $salt * * @return string */ - private function hashPassword($password, $salt) { - return hash_hmac('sha256', $password, $salt); + protected function hashPassword($password, $salt = null) + { + return crypt($password, self::HASH_ALGORITHM . ($salt !== null ? $salt : $this->generateSalt())); } /** @@ -118,12 +167,29 @@ class DbUserBackend extends UserBackend */ public function count() { - $select = new Zend_Db_Select($this->conn->getConnection()); + $select = new Zend_Db_Select($this->conn->getDbAdapter()); $row = $select->from( - 'account', + 'icingaweb_user', array('count' => 'COUNT(*)') )->query()->fetchObject(); return ($row !== false) ? $row->count : 0; } + + /** + * Return the names of all available users + * + * @return array + */ + public function listUsers() + { + $query = $this->conn->select()->from('icingaweb_user', array('name')); + + $users = array(); + foreach ($query->fetchAll() as $row) { + $users[] = $row->name; + } + + return $users; + } } diff --git a/library/Icinga/Authentication/Backend/LdapUserBackend.php b/library/Icinga/Authentication/Backend/LdapUserBackend.php index 3ee3c17dd..519dd3c48 100644 --- a/library/Icinga/Authentication/Backend/LdapUserBackend.php +++ b/library/Icinga/Authentication/Backend/LdapUserBackend.php @@ -4,7 +4,6 @@ namespace Icinga\Authentication\Backend; -use Icinga\Logger\Logger; use Icinga\User; use Icinga\Authentication\UserBackend; use Icinga\Protocol\Ldap\Connection; @@ -20,20 +19,36 @@ class LdapUserBackend extends UserBackend **/ protected $conn; + protected $baseDn; + protected $userClass; protected $userNameAttribute; protected $groupOptions; - public function __construct(Connection $conn, $userClass, $userNameAttribute, $groupOptions = null) + public function __construct(Connection $conn, $userClass, $userNameAttribute, $baseDn, $groupOptions = null) { $this->conn = $conn; + $this->baseDn = trim($baseDn) !== '' ? $baseDn : $conn->getDN(); $this->userClass = $userClass; $this->userNameAttribute = $userNameAttribute; $this->groupOptions = $groupOptions; } + /** + * @return \Icinga\Protocol\Ldap\Query + */ + protected function selectUsers() + { + return $this->conn->select()->setBase($this->baseDn)->from( + $this->userClass, + array( + $this->userNameAttribute + ) + ); + } + /** * Create query * @@ -41,14 +56,9 @@ class LdapUserBackend extends UserBackend * * @return \Icinga\Protocol\Ldap\Query **/ - protected function createQuery($username) + protected function selectUser($username) { - return $this->conn->select() - ->from( - $this->userClass, - array($this->userNameAttribute) - ) - ->where( + return $this->selectUsers()->where( $this->userNameAttribute, str_replace('*', '', $username) ); @@ -68,13 +78,18 @@ class LdapUserBackend extends UserBackend */ public function assertAuthenticationPossible() { - $q = $this->conn->select()->from($this->userClass); - $result = $q->fetchRow(); + try { + $q = $this->conn->select()->setBase($this->baseDn)->from($this->userClass); + $result = $q->fetchRow(); + } catch (LdapException $e) { + throw new AuthenticationException('Connection not possible.', $e); + } + if (! isset($result)) { throw new AuthenticationException( 'No objects with objectClass="%s" in DN="%s" found.', $this->userClass, - $this->conn->getDN() + $this->baseDn ); } @@ -94,12 +109,12 @@ class LdapUserBackend extends UserBackend * * @param string $dn * - * @return array|null + * @return array */ public function getGroups($dn) { if (empty($this->groupOptions) || ! isset($this->groupOptions['group_base_dn'])) { - return null; + return array(); } $q = $this->conn->select() @@ -135,7 +150,7 @@ class LdapUserBackend extends UserBackend public function hasUser(User $user) { $username = $user->getUsername(); - return $this->conn->fetchOne($this->createQuery($username)) === $username; + return $this->conn->fetchOne($this->selectUser($username)) === $username; } /** @@ -158,7 +173,7 @@ class LdapUserBackend extends UserBackend } catch (AuthenticationException $e) { // Authentication not possible throw new AuthenticationException( - 'Authentication against backend "%s" not possible: ', + 'Authentication against backend "%s" not possible.', $this->getName(), $e ); @@ -168,7 +183,7 @@ class LdapUserBackend extends UserBackend return false; } try { - $userDn = $this->conn->fetchDN($this->createQuery($user->getUsername())); + $userDn = $this->conn->fetchDN($this->selectUser($user->getUsername())); $authenticated = $this->conn->testCredentials( $userDn, $password @@ -198,14 +213,20 @@ class LdapUserBackend extends UserBackend */ public function count() { + return $this->conn->count($this->selectUsers()); + } - return $this->conn->count( - $this->conn->select()->from( - $this->userClass, - array( - $this->userNameAttribute - ) - ) - ); + /** + * Return the names of all available users + * + * @return array + */ + public function listUsers() + { + $users = array(); + foreach ($this->selectUsers()->fetchAll() as $row) { + $users[] = $row->{$this->userNameAttribute}; + } + return $users; } } diff --git a/library/Icinga/Authentication/Manager.php b/library/Icinga/Authentication/Manager.php index fa974546d..64d5f5336 100644 --- a/library/Icinga/Authentication/Manager.php +++ b/library/Icinga/Authentication/Manager.php @@ -5,11 +5,10 @@ namespace Icinga\Authentication; use Exception; -use Zend_Config; use Icinga\Application\Config; use Icinga\Exception\IcingaException; use Icinga\Exception\NotReadableError; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\User; use Icinga\User\Preferences; use Icinga\User\Preferences\PreferencesStore; @@ -62,7 +61,7 @@ class Manager $e ) ); - $config = new Zend_Config(array()); + $config = new Config(); } if (($preferencesConfig = $config->preferences) !== null) { try { @@ -120,10 +119,7 @@ class Manager */ public function persistCurrentUser() { - $session = Session::getSession(); - $session->set('user', $this->user); - $session->write(); - $session->refreshId(); + Session::getSession()->set('user', $this->user)->refreshId(); } /** diff --git a/library/Icinga/Authentication/UserBackend.php b/library/Icinga/Authentication/UserBackend.php index 7829210fd..494ada4aa 100644 --- a/library/Icinga/Authentication/UserBackend.php +++ b/library/Icinga/Authentication/UserBackend.php @@ -6,7 +6,7 @@ namespace Icinga\Authentication; use Countable; use Icinga\Authentication\Backend\AutoLoginBackend; -use Zend_Config; +use Icinga\Application\Config; use Icinga\Authentication\Backend\DbUserBackend; use Icinga\Authentication\Backend\LdapUserBackend; use Icinga\Data\ResourceFactory; @@ -45,7 +45,7 @@ abstract class UserBackend implements Countable return $this->name; } - public static function create($name, Zend_Config $backendConfig) + public static function create($name, Config $backendConfig) { if ($backendConfig->name !== null) { $name = $backendConfig->name; @@ -103,6 +103,7 @@ abstract class UserBackend implements Countable $resource, $backendConfig->get('user_class', 'user'), $backendConfig->get('user_name_attribute', 'sAMAccountName'), + $backendConfig->get('base_dn', $resource->getDN()), $groupOptions ); break; @@ -129,6 +130,7 @@ abstract class UserBackend implements Countable $resource, $backendConfig->user_class, $backendConfig->user_name_attribute, + $backendConfig->get('base_dn', $resource->getDN()), $groupOptions ); break; diff --git a/library/Icinga/Authentication/UserGroupBackend.php b/library/Icinga/Authentication/UserGroupBackend.php index e411aee03..b41ef3f13 100644 --- a/library/Icinga/Authentication/UserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroupBackend.php @@ -4,7 +4,7 @@ namespace Icinga\Authentication; -use Zend_Config; +use Icinga\Application\Config; use Icinga\Authentication\Backend\DbUserGroupBackend; use Icinga\Authentication\Backend\IniUserGroupBackend; use Icinga\Data\ResourceFactory; @@ -50,13 +50,13 @@ abstract class UserGroupBackend /** * Create a user group backend * - * @param string $name - * @param Zend_Config $backendConfig + * @param string $name + * @param Config $backendConfig * * @return DbUserGroupBackend|IniUserGroupBackend * @throws ConfigurationError If the backend configuration is invalid */ - public static function create($name, Zend_Config $backendConfig) + public static function create($name, Config $backendConfig) { if ($backendConfig->name !== null) { $name = $backendConfig->name; diff --git a/library/Icinga/Cli/Loader.php b/library/Icinga/Cli/Loader.php index 11ca2b70b..f0aa95499 100644 --- a/library/Icinga/Cli/Loader.php +++ b/library/Icinga/Cli/Loader.php @@ -66,7 +66,7 @@ class Loader public function __construct(App $app) { $this->app = $app; - $this->coreAppDir = ICINGAWEB_APPDIR . '/clicommands'; + $this->coreAppDir = $app->getBaseDir('clicommands'); } /** diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php index 0157bcd72..82686893d 100644 --- a/library/Icinga/Data/Db/DbConnection.php +++ b/library/Icinga/Data/Db/DbConnection.php @@ -4,14 +4,14 @@ namespace Icinga\Data\Db; +use PDO; +use Zend_Db; +use Icinga\Application\Config; use Icinga\Application\Benchmark; use Icinga\Data\Db\DbQuery; use Icinga\Data\ResourceFactory; use Icinga\Data\Selectable; use Icinga\Exception\ConfigurationError; -use PDO; -use Zend_Config; -use Zend_Db; /** * Encapsulate database connections and query creation @@ -21,7 +21,7 @@ class DbConnection implements Selectable /** * Connection config * - * @var Zend_Config + * @var Config */ private $config; @@ -59,9 +59,9 @@ class DbConnection implements Selectable /** * Create a new connection object * - * @param Zend_Config $config + * @param Config $config */ - public function __construct(Zend_Config $config = null) + public function __construct(Config $config = null) { $this->config = $config; if (isset($config->prefix)) { @@ -216,7 +216,10 @@ class DbConnection implements Selectable */ public function fetchRow(DbQuery $query) { - return $this->dbAdapter->fetchRow($query->getSelectQuery()); + Benchmark::measure('DB is fetching row'); + $result = $this->dbAdapter->fetchRow($query->getSelectQuery()); + Benchmark::measure('DB row done'); + return $result; } /** diff --git a/library/Icinga/Data/Filter/Filter.php b/library/Icinga/Data/Filter/Filter.php index 64607a4a0..99b0290f1 100644 --- a/library/Icinga/Data/Filter/Filter.php +++ b/library/Icinga/Data/Filter/Filter.php @@ -79,7 +79,7 @@ abstract class Filter } } - krsort($operators, SORT_NATURAL); + krsort($operators, version_compare(PHP_VERSION, '5.4.0') >= 0 ? SORT_NATURAL : SORT_REGULAR); foreach ($operators as $id => $operator) { $f = $filter->getById($id); if ($f->getOperatorName() !== $operator) { diff --git a/library/Icinga/Data/ResourceFactory.php b/library/Icinga/Data/ResourceFactory.php index 1c443f9d0..f938ea9a5 100644 --- a/library/Icinga/Data/ResourceFactory.php +++ b/library/Icinga/Data/ResourceFactory.php @@ -4,7 +4,6 @@ namespace Icinga\Data; -use Zend_Config; use Icinga\Application\Config; use Icinga\Exception\ProgrammingError; use Icinga\Util\ConfigAwareFactory; @@ -22,14 +21,14 @@ class ResourceFactory implements ConfigAwareFactory /** * Resource configuration * - * @var Zend_Config + * @var Config */ private static $resources; /** * Set resource configurations * - * @param Zend_Config $config + * @param Config $config */ public static function setConfig($config) { @@ -41,7 +40,7 @@ class ResourceFactory implements ConfigAwareFactory * * @param $resourceName String The resource's name * - * @return Zend_Config The configuration of the resource + * @return Config The configuration of the resource * * @throws ConfigurationError */ @@ -62,7 +61,7 @@ class ResourceFactory implements ConfigAwareFactory * * @param String|null $type Fetch only resources that have the given type. * - * @return Zend_Config The configuration containing all resources + * @return Config The configuration containing all resources */ public static function getResourceConfigs($type = null) { @@ -76,7 +75,7 @@ class ResourceFactory implements ConfigAwareFactory $resources[$name] = $resource; } } - return new Zend_Config($resources); + return new Config($resources); } } @@ -100,13 +99,13 @@ class ResourceFactory implements ConfigAwareFactory * NOTE: The factory does not test if the given configuration is valid and the resource is accessible, this * depends entirely on the implementation of the returned resource. * - * @param Zend_Config $config The configuration for the created resource. + * @param Config $config The configuration for the created resource. * * @return DbConnection|LdapConnection|LivestatusConnection An object that can be used to access * the given resource. The returned class depends on the configuration property 'type'. * @throws ConfigurationError When an unsupported type is given */ - public static function createResource(Zend_Config $config) + public static function createResource(Config $config) { switch (strtolower($config->type)) { case 'db': diff --git a/library/Icinga/Config/IniEditor.php b/library/Icinga/File/Ini/IniEditor.php similarity index 98% rename from library/Icinga/Config/IniEditor.php rename to library/Icinga/File/Ini/IniEditor.php index 61639563e..e1a7984d8 100644 --- a/library/Icinga/Config/IniEditor.php +++ b/library/Icinga/File/Ini/IniEditor.php @@ -2,7 +2,7 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -namespace Icinga\Config; +namespace Icinga\File\Ini; /** * Edit the sections and keys of an ini in-place @@ -433,6 +433,9 @@ class IniEditor */ private function formatKey(array $key) { + foreach ($key as $i => $separator) { + $key[$i] = $this->sanitize($separator); + } return implode($this->nestSeparator, $key); } @@ -599,7 +602,7 @@ class IniEditor } /** - * Prepare a value for INe + * Prepare a value for INI * * @param $value The value of the string * @@ -613,10 +616,12 @@ class IniEditor return $value; } elseif (is_bool($value)) { return ($value ? 'true' : 'false'); - } elseif (strpos($value, '"') === false) { - return '"' . $value . '"'; - } else { - return '"' . str_replace('"', '\"', $value) . '"'; } + return '"' . str_replace('"', '\"', $this->sanitize($value)) . '"'; + } + + private function sanitize($value) + { + return str_replace('\n', '', $value); } } diff --git a/library/Icinga/Config/PreservingIniWriter.php b/library/Icinga/File/Ini/IniWriter.php similarity index 76% rename from library/Icinga/Config/PreservingIniWriter.php rename to library/Icinga/File/Ini/IniWriter.php index 2cafd5416..cf9355b56 100644 --- a/library/Icinga/Config/PreservingIniWriter.php +++ b/library/Icinga/File/Ini/IniWriter.php @@ -2,17 +2,18 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -namespace Icinga\Config; +namespace Icinga\File\Ini; use Zend_Config; use Zend_Config_Ini; +use Zend_Config_Exception; use Zend_Config_Writer_FileAbstract; -use Icinga\Config\IniEditor; +use Icinga\Application\Config; /** - * A ini file adapter that respects the file structure and the comments of already existing ini files + * A INI file adapter that respects the file structure and the comments of already existing ini files */ -class PreservingIniWriter extends Zend_Config_Writer_FileAbstract +class IniWriter extends Zend_Config_Writer_FileAbstract { /** * Stores the options @@ -22,10 +23,17 @@ class PreservingIniWriter extends Zend_Config_Writer_FileAbstract protected $options; /** - * Create a new PreservingIniWriter + * The mode to set on new files * - * @param array $options Supports all options of Zend_Config_Writer and additional - * options for the internal IniEditor: + * @var int + */ + public static $fileMode = 0664; + + /** + * Create a new INI writer + * + * @param array $options Supports all options of Zend_Config_Writer and additional options: + * * filemode: The mode to set on new files * * valueIndentation: The indentation level of the values * * commentIndentation: The indentation level of the comments * * sectionSeparators: The amount of newlines between sections @@ -34,49 +42,16 @@ class PreservingIniWriter extends Zend_Config_Writer_FileAbstract */ public function __construct(array $options = null) { + if (isset($options['config']) && $options['config'] instanceof Config) { + // As this class inherits from Zend_Config_Writer_FileAbstract we must + // not pass the config directly as it needs to be of type Zend_Config + $options['config'] = new Zend_Config($options['config']->toArray(), true); + } + $this->options = $options; parent::__construct($options); } - /** - * Find all keys containing dots and convert it to a nested configuration - * - * Ensure that configurations with the same ini representation the have - * similarly nested Zend_Config objects. The configuration may be altered - * during that process. - * - * @param Zend_Config $config The configuration to normalize - * @return Zend_Config The normalized config - */ - private function normalizeKeys(Zend_Config $config) - { - foreach ($config as $key => $value) { - if (preg_match('/\./', $key) > 0) { - // remove old key - unset ($config->$key); - - // insert new key - $nests = explode('.', $key); - $current = $config; - $i = 0; - for (; $i < count($nests) - 1; $i++) { - if (! isset($current->{$nests[$i]})) { - // configuration key doesn't exist, create a new nesting level - $current->{$nests[$i]} = new Zend_Config (array(), true); - } - // move to next nesting level - $current = $current->{$nests[$i]}; - } - // reached last nesting level, insert value - $current->{$nests[$i]} = $value; - } - if ($value instanceof Zend_Config) { - $config->$key = $this->normalizeKeys ($value); - } - } - return $config; - } - /** * Render the Zend_Config into a config file string * @@ -90,23 +65,37 @@ class PreservingIniWriter extends Zend_Config_Writer_FileAbstract $oldconfig = new Zend_Config(array()); } - // create an internal copy of the given configuration, since the user of this class - // won't expect that a configuration will ever be altered during - // the rendering process. - $extends = $this->_config->getExtends(); - $this->_config = new Zend_Config ($this->_config->toArray(), true); - foreach ($extends as $extending => $extended) { - $this->_config->setExtend($extending, $extended); - } - $this->_config = $this->normalizeKeys($this->_config); - $newconfig = $this->_config; - $editor = new IniEditor(file_get_contents($this->_filename), $this->options); + $editor = new IniEditor(@file_get_contents($this->_filename), $this->options); $this->diffConfigs($oldconfig, $newconfig, $editor); $this->updateSectionOrder($newconfig, $editor); return $editor->getText(); } + /** + * Write configuration to file and set file mode in case it does not exist yet + * + * @param string $filename + * @param Zend_Config $config + * @param bool $exclusiveLock + */ + public function write($filename = null, Zend_Config $config = null, $exclusiveLock = null) + { + $filePath = $filename !== null ? $filename : $this->_filename; + $setMode = false === file_exists($filePath); + + parent::write($filename, $config, $exclusiveLock); + + if ($setMode) { + $mode = isset($this->options['filemode']) ? $this->options['filemode'] : static::$fileMode; + $old = umask(0); // Make sure that the mode we're going to set doesn't get mangled + if (is_int($mode) && false === @chmod($filePath, $mode)) { + throw new Zend_Config_Exception(sprintf('Failed to set file mode "%o" on file "%s"', $mode, $filePath)); + } + umask($old); + } + } + /** * Create a property diff and apply the changes to the editor * diff --git a/library/Icinga/File/Pdf.php b/library/Icinga/File/Pdf.php index 2e8a97887..6f4102129 100644 --- a/library/Icinga/File/Pdf.php +++ b/library/Icinga/File/Pdf.php @@ -49,7 +49,7 @@ class Pdf extends DOMPDF $layout->content = $controller->getResponse(); $html = $layout->render(); $imgDir = Url::fromPath('img'); - $html = preg_replace('~src="' . $imgDir . '/~', 'src="' . Icinga::app()->getBootstrapDirecory() . '/img/', $html); + $html = preg_replace('~src="' . $imgDir . '/~', 'src="' . Icinga::app()->getBootstrapDirectory() . '/img/', $html); $html = preg_replace('~src="/svg/chart.php(.*)"~', 'class="icon" src="http://master1.com/png/chart.php$1"', $html); $this->load_html($html); $this->render(); diff --git a/library/Icinga/Protocol/Dns.php b/library/Icinga/Protocol/Dns.php index 92e8b2a4d..950c22f44 100644 --- a/library/Icinga/Protocol/Dns.php +++ b/library/Icinga/Protocol/Dns.php @@ -9,7 +9,6 @@ namespace Icinga\Protocol; */ class Dns { - /** * Discover all service records on a given domain * @@ -17,21 +16,12 @@ class Dns * @param string $service The type of the service, like for example 'ldaps' or 'ldap' * @param string $protocol The transport protocol used by the service, defaults to 'tcp' * - * @return array|null An array of all service domains + * @return array An array of all found service records */ public static function getSrvRecords($domain, $service, $protocol = 'tcp') { $records = dns_get_record('_' . $service . '._' . $protocol . '.' . $domain, DNS_SRV); - if ($records === false) { - return null; - } - $targets = array(); - foreach ($records as $record) { - if (array_key_exists('target', $record)) { - $targets[] = $record['target']; - } - } - return $targets; + return $records === false ? array() : $records; } /** diff --git a/library/Icinga/Protocol/File/FileReader.php b/library/Icinga/Protocol/File/FileReader.php index 49da989ef..26445de8a 100644 --- a/library/Icinga/Protocol/File/FileReader.php +++ b/library/Icinga/Protocol/File/FileReader.php @@ -4,9 +4,9 @@ namespace Icinga\Protocol\File; -use Icinga\Data\Selectable; use Countable; -use Zend_Config; +use Icinga\Application\Config; +use Icinga\Data\Selectable; /** * Read file line by line @@ -30,11 +30,11 @@ class FileReader implements Selectable, Countable /** * Create a new reader * - * @param Zend_Config $config + * @param Config $config * * @throws FileReaderException If a required $config directive (filename or fields) is missing */ - public function __construct(Zend_Config $config) + public function __construct(Config $config) { foreach (array('filename', 'fields') as $key) { if (isset($config->{$key})) { diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 6fa41e2aa..1cd66c70a 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -7,8 +7,7 @@ namespace Icinga\Protocol\Ldap; use Icinga\Protocol\Ldap\Exception as LdapException; use Icinga\Application\Platform; use Icinga\Application\Config; -use Icinga\Logger\Logger; -use Zend_Config; +use Icinga\Application\Logger; /** * Backend class managing all the LDAP stuff for you. @@ -101,9 +100,9 @@ class Connection * * TODO: Allow to pass port and SSL options * - * @param Zend_Config $config + * @param Config $config */ - public function __construct(Zend_Config $config) + public function __construct(Config $config) { $this->hostname = $config->hostname; $this->bind_dn = $config->bind_dn; @@ -336,9 +335,9 @@ class Connection public function testCredentials($username, $password) { - $ds = $this->prepareNewConnection(); + $this->connect(); - $r = @ldap_bind($ds, $username, $password); + $r = @ldap_bind($this->ds, $username, $password); if ($r) { Logger::debug( 'Successfully tested LDAP credentials (%s / %s)', @@ -351,7 +350,7 @@ class Connection 'Testing LDAP credentials (%s / %s) failed: %s', $username, '***', - ldap_error($ds) + ldap_error($this->ds) ); return false; } @@ -364,7 +363,7 @@ class Connection */ protected function getConfigDir($sub = null) { - $dir = Config::getInstance()->getConfigDir() . '/ldap'; + $dir = Config::$configDir . '/ldap'; if ($sub !== null) { $dir .= '/' . $sub; } @@ -388,7 +387,19 @@ class Connection } $ds = ldap_connect($this->hostname, $this->port); - list($cap, $namingContexts) = $this->discoverCapabilities($ds); + try { + $capabilities = $this->discoverCapabilities($ds); + list($cap, $namingContexts) = $capabilities; + } catch (LdapException $e) { + + // discovery failed, guess defaults + $cap = (object) array( + 'supports_ldapv3' => true, + 'supports_starttls' => false, + 'msCapabilities' => array() + ); + $namingContexts = null; + } $this->capabilities = $cap; $this->namingContexts = $namingContexts; @@ -626,7 +637,8 @@ class Connection if (! $result) { throw new LdapException( sprintf( - 'Capability query failed (%s:%d): %s', + 'Capability query failed (%s:%d): %s. Check if hostname and port of the ldap resource are correct ' + . ' and if anonymous access is permitted.', $this->hostname, $this->port, ldap_error($ds) @@ -634,6 +646,16 @@ class Connection ); } $entry = ldap_first_entry($ds, $result); + if ($entry === false) { + throw new LdapException( + sprintf( + 'Capabilities not available (%s:%d): %s. Discovery of root DSE probably not permitted.', + $this->hostname, + $this->port, + ldap_error($ds) + ) + ); + } $cap = (object) array( 'supports_ldapv3' => false, @@ -641,10 +663,6 @@ class Connection 'msCapabilities' => array() ); - if ($entry === false) { - // TODO: Is it OK to have no capabilities? - return false; - } $ldapAttributes = ldap_get_attributes($ds, $entry); $result = $this->cleanupAttributes($ldapAttributes); $cap->supports_ldapv3 = $this->hasCapabilityLdapV3($result); diff --git a/library/Icinga/Test/BaseTestCase.php b/library/Icinga/Test/BaseTestCase.php index a735550cb..ade096547 100644 --- a/library/Icinga/Test/BaseTestCase.php +++ b/library/Icinga/Test/BaseTestCase.php @@ -24,9 +24,9 @@ namespace Icinga\Test { use Exception; use RuntimeException; use Mockery; - use Zend_Config; use PHPUnit_Framework_TestCase; use Icinga\Application\Icinga; + use Icinga\Application\Config; use Icinga\Util\DateTimeFactory; use Icinga\Data\ResourceFactory; use Icinga\Data\Db\DbConnection; @@ -191,17 +191,17 @@ namespace Icinga\Test { } /** - * Create Zend_Config for database configuration + * Create Config for database configuration * * @param string $name * - * @return Zend_Config + * @return Config * @throws RuntimeException */ protected function createDbConfigFor($name) { if (array_key_exists($name, self::$dbConfiguration)) { - return new Zend_Config(self::$dbConfiguration[$name]); + return new Config(self::$dbConfiguration[$name]); } throw new RuntimeException('Configuration for database type not available: ' . $name); diff --git a/library/Icinga/User/Preferences/PreferencesStore.php b/library/Icinga/User/Preferences/PreferencesStore.php index b8121eb25..79bea2e40 100644 --- a/library/Icinga/User/Preferences/PreferencesStore.php +++ b/library/Icinga/User/Preferences/PreferencesStore.php @@ -4,13 +4,12 @@ namespace Icinga\User\Preferences; -use Zend_Config; +use Icinga\Application\Config; use Icinga\User; use Icinga\User\Preferences; use Icinga\Data\ResourceFactory; use Icinga\Exception\ConfigurationError; use Icinga\Data\Db\DbConnection; -use Icinga\Application\Config as IcingaConfig; /** * Preferences store factory @@ -19,13 +18,13 @@ use Icinga\Application\Config as IcingaConfig; * * 'ini', * 'config_path' => '/path/to/preferences' * ), @@ -42,7 +41,7 @@ abstract class PreferencesStore /** * Store config * - * @var Zend_Config + * @var Config */ protected $config; @@ -56,10 +55,10 @@ abstract class PreferencesStore /** * Create a new store * - * @param Zend_Config $config The config for this adapter - * @param User $user The user to which these preferences belong + * @param Config $config The config for this adapter + * @param User $user The user to which these preferences belong */ - public function __construct(Zend_Config $config, User $user) + public function __construct(Config $config, User $user) { $this->config = $config; $this->user = $user; @@ -69,7 +68,7 @@ abstract class PreferencesStore /** * Getter for the store config * - * @return Zend_Config + * @return Config */ public function getStoreConfig() { @@ -108,14 +107,14 @@ abstract class PreferencesStore /** * Create preferences storage adapter from config * - * @param Zend_Config $config The config for the adapter - * @param User $user The user to which these preferences belong + * @param Config $config The config for the adapter + * @param User $user The user to which these preferences belong * * @return self * * @throws ConfigurationError When the configuration defines an invalid storage type */ - public static function create(Zend_Config $config, User $user) + public static function create(Config $config, User $user) { if (($type = $config->type) === null) { throw new ConfigurationError( @@ -133,7 +132,7 @@ abstract class PreferencesStore } if ($type === 'Ini') { - $config->location = IcingaConfig::resolvePath('preferences'); + $config->location = Config::resolvePath('preferences'); } elseif ($type === 'Db') { $config->connection = new DbConnection(ResourceFactory::getResourceConfig($config->resource)); } diff --git a/library/Icinga/User/Preferences/Store/IniStore.php b/library/Icinga/User/Preferences/Store/IniStore.php index 1f01925c9..c668b0bc0 100644 --- a/library/Icinga/User/Preferences/Store/IniStore.php +++ b/library/Icinga/User/Preferences/Store/IniStore.php @@ -4,13 +4,13 @@ namespace Icinga\User\Preferences\Store; -use Zend_Config; -use Icinga\Util\File; -use Icinga\Config\PreservingIniWriter; +use Icinga\Application\Config; use Icinga\Exception\NotReadableError; use Icinga\Exception\NotWritableError; +use Icinga\File\Ini\IniWriter; use Icinga\User\Preferences; use Icinga\User\Preferences\PreferencesStore; +use Icinga\Util\File; /** * Load and save user preferences from and to INI files @@ -34,7 +34,7 @@ class IniStore extends PreferencesStore /** * Writer which stores the preferences * - * @var PreservingIniWriter + * @var IniWriter */ protected $writer; @@ -114,9 +114,9 @@ class IniStore extends PreferencesStore ); } - $this->writer = new PreservingIniWriter( + $this->writer = new IniWriter( array( - 'config' => new Zend_Config($this->preferences), + 'config' => new Config($this->preferences), 'filename' => $this->preferencesFile ) ); diff --git a/library/Icinga/Util/Translator.php b/library/Icinga/Util/Translator.php index 755203474..52a84bd62 100644 --- a/library/Icinga/Util/Translator.php +++ b/library/Icinga/Util/Translator.php @@ -224,6 +224,7 @@ class Translator } } } + sort($codes); return $codes; } diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php index 315209232..a41670159 100644 --- a/library/Icinga/Web/Controller/ActionController.php +++ b/library/Icinga/Web/Controller/ActionController.php @@ -18,8 +18,6 @@ use Icinga\File\Pdf; use Icinga\Exception\ProgrammingError; use Icinga\Web\Session; use Icinga\Web\UrlParams; -use Icinga\Session\SessionNamespace; -use Icinga\Exception\NotReadableError; use Zend_Controller_Action; use Zend_Controller_Action_HelperBroker as ActionHelperBroker; use Zend_Controller_Request_Abstract as Request; @@ -325,14 +323,24 @@ class ActionController extends Zend_Controller_Action $this->getResponse()->setHeader('X-Icinga-Reload-Css', 'now'); } + $this->shutdownSession(); + $this->getResponse() ->setHeader('X-Icinga-Redirect', rawurlencode($url->getAbsoluteUrl())) ->sendHeaders(); - // TODO: Session shutdown? exit; } + protected function redirectHttp($url) + { + if (! $url instanceof Url) { + $url = Url::fromPath($url); + } + $this->shutdownSession(); + $this->_helper->Redirector->gotoUrlAndExit($url->getRelativeUrl()); + } + /** * Redirect to a specific url, updating the browsers URL field * @@ -343,10 +351,7 @@ class ActionController extends Zend_Controller_Action if ($this->isXhr()) { $this->redirectXhr($url); } else { - if (! $url instanceof Url) { - $url = Url::fromPath($url); - } - $this->_helper->Redirector->gotoUrlAndExit($url->getRelativeUrl()); + $this->redirectHttp($url); } } @@ -374,6 +379,7 @@ class ActionController extends Zend_Controller_Action if ($req->getParam('format') === 'pdf') { $layout->setLayout('pdf'); + $this->shutdownSession(); $this->sendAsPdf(); exit; } @@ -381,6 +387,8 @@ class ActionController extends Zend_Controller_Action if ($this->isXhr()) { $this->postDispatchXhr(); } + + $this->shutdownSession(); } protected function postDispatchXhr() @@ -430,6 +438,14 @@ class ActionController extends Zend_Controller_Action $pdf->renderControllerAction($this); } + protected function shutdownSession() + { + $session = Session::getSession(); + if ($session->hasChanged()) { + $session->write(); + } + } + /** * Render the benchmark * diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php index 631b2eb07..f315c264e 100644 --- a/library/Icinga/Web/Form.php +++ b/library/Icinga/Web/Form.php @@ -100,11 +100,11 @@ class Form extends Zend_Form * @var array */ public static $defaultElementDecorators = array( - 'ViewHelper', - 'Errors', - array('Description', array('tag' => 'span', 'class' => 'description')), - 'Label', - array('HtmlTag', array('tag' => 'div')) + array('ViewHelper', array('separator' => '')), + array('Errors', array('separator' => '')), + array('Description', array('tag' => 'span', 'class' => 'description', 'separator' => '')), + array('Label', array('separator' => '')), + array('HtmlTag', array('tag' => 'div', 'class' => 'element')) ); /** @@ -463,7 +463,15 @@ class Form extends Zend_Form $el = parent::createElement($type, $name, $options); if ($el && $el->getAttrib('autosubmit')) { - $el->addDecorator(new NoScriptApply()); // Non-JS environments + $noScript = new NoScriptApply(); // Non-JS environments + $decorators = $el->getDecorators(); + $pos = array_search('Zend_Form_Decorator_ViewHelper', array_keys($decorators)) + 1; + $el->setDecorators( + array_slice($decorators, 0, $pos, true) + + array(get_class($noScript) => $noScript) + + array_slice($decorators, $pos, count($decorators) - $pos, true) + ); + $class = $el->getAttrib('class'); if (is_array($class)) { $class[] = 'autosubmit'; @@ -473,6 +481,7 @@ class Form extends Zend_Form $class .= ' autosubmit'; } $el->setAttrib('class', $class); // JS environments + unset($el->autosubmit); } diff --git a/library/Icinga/Web/Form/Decorator/ElementDoubler.php b/library/Icinga/Web/Form/Decorator/ElementDoubler.php new file mode 100644 index 000000000..8bedf9492 --- /dev/null +++ b/library/Icinga/Web/Form/Decorator/ElementDoubler.php @@ -0,0 +1,64 @@ +getElement(); + if ($group->getElement($this->getOption('condition')) !== null) { + if ($this->getPlacement() === static::APPEND) { + return $content . $this->applyAttributes($group->getElement($this->getOption('double')))->render(); + } else { // $this->getPlacement() === static::PREPEND + return $this->applyAttributes($group->getElement($this->getOption('double')))->render() . $content; + } + } + + return $content; + } + + /** + * Apply all element attributes + * + * @param Zend_Form_Element $element The element to apply the attributes to + * + * @return Zend_Form_Element + */ + protected function applyAttributes(Zend_Form_Element $element) + { + $attributes = $this->getOption('attributes'); + if ($attributes !== null) { + foreach ($attributes as $name => $value) { + $element->setAttrib($name, $value); + } + } + + return $element; + } +} diff --git a/library/Icinga/Web/Form/Element/Note.php b/library/Icinga/Web/Form/Element/Note.php index 78881ab44..fa003a2cf 100644 --- a/library/Icinga/Web/Form/Element/Note.php +++ b/library/Icinga/Web/Form/Element/Note.php @@ -5,7 +5,6 @@ namespace Icinga\Web\Form\Element; use Zend_Form_Element; -use Icinga\Web\Form; /** * A note @@ -32,7 +31,15 @@ class Note extends Zend_Form_Element */ public function init() { - $this->setDecorators(Form::$defaultElementDecorators); + if (count($this->getDecorators()) === 0) { + $this->setDecorators(array( + 'ViewHelper', + array( + 'HtmlTag', + array('tag' => 'p') + ) + )); + } } /** diff --git a/library/Icinga/Web/Form/Validator/WritablePathValidator.php b/library/Icinga/Web/Form/Validator/WritablePathValidator.php index 8387c5cf5..239ab77c8 100644 --- a/library/Icinga/Web/Form/Validator/WritablePathValidator.php +++ b/library/Icinga/Web/Form/Validator/WritablePathValidator.php @@ -5,7 +5,6 @@ namespace Icinga\Web\Form\Validator; use Zend_Validate_Abstract; -use Icinga\Application\Config as IcingaConfig; /** * Validator that interprets the value as a path and checks if it's writable diff --git a/library/Icinga/Web/Hook.php b/library/Icinga/Web/Hook.php index 36974fb5b..01d1e0e2c 100644 --- a/library/Icinga/Web/Hook.php +++ b/library/Icinga/Web/Hook.php @@ -5,7 +5,7 @@ namespace Icinga\Web; use Exception; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Exception\ProgrammingError; /** diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php index 6224b5b82..0b64a394b 100644 --- a/library/Icinga/Web/JavaScript.php +++ b/library/Icinga/Web/JavaScript.php @@ -67,7 +67,7 @@ class JavaScript public static function send($minified = false) { header('Content-Type: application/javascript'); - $basedir = Icinga::app()->getBootstrapDirecory(); + $basedir = Icinga::app()->getBootstrapDirectory(); $js = $out = ''; $min = $minified ? '.min' : ''; diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index 305cdd705..4cd660116 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -6,10 +6,9 @@ namespace Icinga\Web; use Icinga\Web\Menu\MenuItemRenderer; use RecursiveIterator; -use Zend_Config; use Icinga\Application\Config; use Icinga\Application\Icinga; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Exception\ConfigurationError; use Icinga\Exception\ProgrammingError; use Icinga\Web\Url; @@ -79,10 +78,10 @@ class Menu implements RecursiveIterator /** * Create a new menu * - * @param int $id The id of this menu - * @param Zend_Config $config The configuration for this menu + * @param int $id The id of this menu + * @param Config $config The configuration for this menu */ - public function __construct($id, Zend_Config $config = null, Menu $parent = null) + public function __construct($id, Config $config = null, Menu $parent = null) { $this->id = $id; if ($parent !== null) { @@ -94,7 +93,7 @@ class Menu implements RecursiveIterator /** * Set all given properties * - * @param array|Zend_Config $props Property list + * @param array|Config $props Property list */ public function setProperties($props = null) { @@ -415,11 +414,11 @@ class Menu implements RecursiveIterator * Add a sub menu to this menu * * @param string $id The id of the menu to add - * @param Zend_Config $itemConfig The config with which to initialize the menu + * @param Config $itemConfig The config with which to initialize the menu * * @return self */ - public function addSubMenu($id, Zend_Config $menuConfig = null) + public function addSubMenu($id, Config $menuConfig = null) { if (false === ($pos = strpos($id, '.'))) { $subMenu = new self($id, $menuConfig, $this); @@ -509,7 +508,7 @@ class Menu implements RecursiveIterator */ public function add($name, $config = array()) { - return $this->addSubMenu($name, new Zend_Config($config)); + return $this->addSubMenu($name, new Config($config)); } /** diff --git a/library/Icinga/Web/Menu/MonitoringMenuItemRenderer.php b/library/Icinga/Web/Menu/MonitoringMenuItemRenderer.php new file mode 100644 index 000000000..63e819d1b --- /dev/null +++ b/library/Icinga/Web/Menu/MonitoringMenuItemRenderer.php @@ -0,0 +1,90 @@ +select()->from( + 'statusSummary', + array( + 'hosts_down_unhandled', + 'services_critical_unhandled' + ) + )->getQuery()->fetchRow(); + } + + if ($column === null) { + return self::$summary; + } elseif (isset(self::$summary->$column)) { + return self::$summary->$column; + } else { + return null; + } + } + + protected function getBadgeTitle() + { + $translations = array( + 'hosts_down_unhandled' => mt('monitoring', '%d unhandled hosts down'), + 'services_critical_unhandled' => mt('monitoring', '%d unhandled services critical') + ); + + $titles = array(); + $sum = $this->summary(); + + foreach ($this->columns as $col) { + if (isset($sum->$col) && $sum->$col > 0) { + $titles[] = sprintf($translations[$col], $sum->$col); + } + } + + return implode(', ', $titles); + } + + protected function countItems() + { + $sum = self::summary(); + $count = 0; + + foreach ($this->columns as $col) { + if (isset($sum->$col)) { + $count += $sum->$col; + } + } + + return $count; + } + + public function render(Menu $menu) + { + $count = $this->countItems(); + $badge = ''; + if ($count) { + $badge = sprintf( + '
%s
', + $this->getBadgeTitle(), + $count + ); + } + return sprintf( + '%s%s%s', + $menu->getUrl() ?: '#', + $menu->getIcon() ? ' ' : '', + htmlspecialchars($menu->getTitle()), + $badge + ); + } +} diff --git a/library/Icinga/Web/Menu/ProblemMenuItemRenderer.php b/library/Icinga/Web/Menu/ProblemMenuItemRenderer.php index 1254033ae..58689547f 100644 --- a/library/Icinga/Web/Menu/ProblemMenuItemRenderer.php +++ b/library/Icinga/Web/Menu/ProblemMenuItemRenderer.php @@ -1,39 +1,11 @@ select()->from( - 'statusSummary', - array( - 'hosts_down_unhandled', - 'services_critical_unhandled' - ) - )->getQuery()->fetchRow(); - $unhandled = $statusSummary->hosts_down_unhandled + $statusSummary->services_critical_unhandled; - $badge = ''; - if ($unhandled) { - $badge = sprintf( - '
%s
', - $unhandled - ); - } - return sprintf( - '%s%s %s', - $menu->getUrl() ?: '#', - $menu->getIcon() ? ' ' : '', - htmlspecialchars($menu->getTitle()), - $badge - ); - } +class ProblemMenuItemRenderer extends MonitoringMenuItemRenderer +{ + protected $columns = array( + 'hosts_down_unhandled', + 'services_critical_unhandled' + ); } diff --git a/library/Icinga/Web/Menu/UnhandledHostMenuItemRenderer.php b/library/Icinga/Web/Menu/UnhandledHostMenuItemRenderer.php index c7e6036fb..10a75c00f 100644 --- a/library/Icinga/Web/Menu/UnhandledHostMenuItemRenderer.php +++ b/library/Icinga/Web/Menu/UnhandledHostMenuItemRenderer.php @@ -1,38 +1,12 @@ select()->from( - 'statusSummary', - array( - 'hosts_down_unhandled' - ) - )->getQuery()->fetchRow(); - $badge = ''; - if ($statusSummary->hosts_down_unhandled) { - $badge = sprintf( - '
%s
', - t(sprintf('%d unhandled host problems', $statusSummary->hosts_down_unhandled)), - $statusSummary->hosts_down_unhandled - ); - } - return sprintf( - '%s%s %s', - $menu->getUrl() ?: '#', - $menu->getIcon() ? ' ' : '', - htmlspecialchars($menu->getTitle()), - $badge - ); - } +class UnhandledHostMenuItemRenderer extends MonitoringMenuItemRenderer +{ + protected $columns = array( + 'hosts_down_unhandled', + ); } diff --git a/library/Icinga/Web/Menu/UnhandledServiceMenuItemRenderer.php b/library/Icinga/Web/Menu/UnhandledServiceMenuItemRenderer.php index b677a4935..8a59e9baa 100644 --- a/library/Icinga/Web/Menu/UnhandledServiceMenuItemRenderer.php +++ b/library/Icinga/Web/Menu/UnhandledServiceMenuItemRenderer.php @@ -1,38 +1,12 @@ select()->from( - 'statusSummary', - array( - 'services_critical_unhandled' - ) - )->getQuery()->fetchRow(); - $badge = ''; - if ($statusSummary->services_critical_unhandled) { - $badge = sprintf( - '
%s
', - t(sprintf('%d unhandled service problems', $statusSummary->services_critical_unhandled)), - $statusSummary->services_critical_unhandled - ); - } - return sprintf( - '%s%s %s', - $menu->getUrl() ?: '#', - $menu->getIcon() ? ' ' : '', - htmlspecialchars($menu->getTitle()), - $badge - ); - } +class UnhandledServiceMenuItemRenderer extends MonitoringMenuItemRenderer +{ + protected $columns = array( + 'services_critical_unhandled' + ); } diff --git a/library/Icinga/Web/MenuRenderer.php b/library/Icinga/Web/MenuRenderer.php index 2fbcedc01..bbd5c208d 100644 --- a/library/Icinga/Web/MenuRenderer.php +++ b/library/Icinga/Web/MenuRenderer.php @@ -6,7 +6,7 @@ namespace Icinga\Web; use Exception; use RecursiveIteratorIterator; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; /** * A renderer to draw a menu with its sub-menus using an unordered html list diff --git a/library/Icinga/Web/Notification.php b/library/Icinga/Web/Notification.php index 178a07e0a..b046476b3 100644 --- a/library/Icinga/Web/Notification.php +++ b/library/Icinga/Web/Notification.php @@ -6,7 +6,7 @@ namespace Icinga\Web; use Icinga\Exception\ProgrammingError; use Icinga\Application\Platform; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Web\Session; /** @@ -75,35 +75,28 @@ class Notification return; } - $mo = (object) array( + $messages = & Session::getSession()->getByRef('messages'); + $messages[] = (object) array( 'type' => $type, 'message' => $message, ); - - // Get, change, set - just to be on the safe side: - $session = Session::getSession(); - $msgs = $session->messages; - $msgs[] = $mo; - $session->messages = $msgs; - $session->write(); } public function hasMessages() { $session = Session::getSession(); - return !empty($session->messages); + return false === empty($session->messages); } public function getMessages() { $session = Session::getSession(); - $msgs = $session->messages; - if (false === empty($msgs)) { + $messages = $session->messages; + if (false === empty($messages)) { $session->messages = array(); - $session->write(); } - return $msgs; + return $messages; } final private function __construct() diff --git a/library/Icinga/Web/Response.php b/library/Icinga/Web/Response.php index 6ad4f6f87..d0d8912e6 100644 --- a/library/Icinga/Web/Response.php +++ b/library/Icinga/Web/Response.php @@ -21,6 +21,12 @@ class Response extends Zend_Controller_Response_Http } else { $this->setRedirect($url->getAbsoluteUrl()); } + + $session = Session::getSession(); + if ($session->hasChanged()) { + $session->write(); + } + $this->sendHeaders(); exit; } diff --git a/library/Icinga/Web/Session/PhpSession.php b/library/Icinga/Web/Session/PhpSession.php index 65f940a13..bef978c0b 100644 --- a/library/Icinga/Web/Session/PhpSession.php +++ b/library/Icinga/Web/Session/PhpSession.php @@ -4,7 +4,7 @@ namespace Icinga\Web\Session; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Exception\ConfigurationError; /** @@ -121,7 +121,7 @@ class PhpSession extends Session foreach ($_SESSION as $key => $value) { if (strpos($key, self::NAMESPACE_PREFIX) === 0) { - $namespace = new SessionNamespace($this); + $namespace = new SessionNamespace(); $namespace->setAll($value); $this->namespaces[substr($key, strlen(self::NAMESPACE_PREFIX))] = $namespace; } else { diff --git a/library/Icinga/Web/Session/Session.php b/library/Icinga/Web/Session/Session.php index 0c60d7f98..ab73dcac7 100644 --- a/library/Icinga/Web/Session/Session.php +++ b/library/Icinga/Web/Session/Session.php @@ -75,7 +75,7 @@ abstract class Session extends SessionNamespace unset($this->removedNamespaces[array_search($identifier, $this->removedNamespaces)]); } - $this->namespaces[$identifier] = new SessionNamespace($this); + $this->namespaces[$identifier] = new SessionNamespace(); } return $this->namespaces[$identifier]; @@ -104,13 +104,22 @@ abstract class Session extends SessionNamespace $this->removedNamespaces[] = $identifier; } + /** + * Return whether the session has changed + * + * @return bool + */ + public function hasChanged() + { + return parent::hasChanged() || false === empty($this->namespaces) || false === empty($this->removedNamespaces); + } + /** * Clear all values and namespaces from the session cache */ public function clear() { - $this->values = array(); - $this->removed = array(); + parent::clear(); $this->namespaces = array(); $this->removedNamespaces = array(); } diff --git a/library/Icinga/Web/Session/SessionNamespace.php b/library/Icinga/Web/Session/SessionNamespace.php index b7495683b..c72d69c36 100644 --- a/library/Icinga/Web/Session/SessionNamespace.php +++ b/library/Icinga/Web/Session/SessionNamespace.php @@ -14,13 +14,6 @@ use IteratorAggregate; */ class SessionNamespace implements IteratorAggregate { - /** - * The session this namespace is associated to - * - * @var Session - */ - protected $session; - /** * The actual values stored in this container * @@ -35,16 +28,6 @@ class SessionNamespace implements IteratorAggregate */ protected $removed = array(); - /** - * Create a new session namespace - * - * @param Session $session The session this namespace is associated to - */ - public function __construct(Session $session = null) - { - $this->session = $session; - } - /** * Return an iterator for all values in this namespace * @@ -120,7 +103,18 @@ class SessionNamespace implements IteratorAggregate $this->values[$key] = $value; if (in_array($key, $this->removed)) { - unset($this->removed[array_search($key, $this->values)]); + unset($this->removed[array_search($key, $this->removed)]); + } + + return $this; + } + + public function setByRef($key, &$value) + { + $this->values[$key] = & $value; + + if (in_array($key, $this->removed)) { + unset($this->removed[array_search($key, $this->removed)]); } return $this; @@ -139,6 +133,16 @@ class SessionNamespace implements IteratorAggregate return isset($this->values[$key]) ? $this->values[$key] : $default; } + public function & getByRef($key, $default = null) + { + $value = $default; + if (isset($this->values[$key])) { + $value = & $this->values[$key]; + } + + return $value; + } + /** * Delete the given value from the session * @@ -177,14 +181,21 @@ class SessionNamespace implements IteratorAggregate } /** - * Save the session this namespace is associated to + * Return whether the session namespace has been changed + * + * @return bool */ - public function write() + public function hasChanged() { - if (!$this->session) { - throw new IcingaException('Cannot save, session not set'); - } + return false === empty($this->values) || false === empty($this->removed); + } - $this->session->write(); + /** + * Clear all values from the session namespace + */ + public function clear() + { + $this->values = array(); + $this->removed = array(); } } diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php index 3c4baf943..062b7d86f 100644 --- a/library/Icinga/Web/StyleSheet.php +++ b/library/Icinga/Web/StyleSheet.php @@ -19,6 +19,7 @@ class StyleSheet 'css/icinga/main-content.less', 'css/icinga/tabs.less', 'css/icinga/forms.less', + 'css/icinga/setup.less', 'css/icinga/widgets.less', 'css/icinga/pagination.less', 'css/icinga/monitoring-colors.less', @@ -30,7 +31,7 @@ class StyleSheet public static function compileForPdf() { $less = new LessCompiler(); - $basedir = Icinga::app()->getBootstrapDirecory(); + $basedir = Icinga::app()->getBootstrapDirectory(); foreach (self::$lessFiles as $file) { $less->addFile($basedir . '/' . $file); } @@ -56,7 +57,7 @@ class StyleSheet public static function send($minified = false) { $app = Icinga::app(); - $basedir = $app->getBootstrapDirecory(); + $basedir = $app->getBootstrapDirectory(); foreach (self::$lessFiles as $file) { $lessFiles[] = $basedir . '/' . $file; } diff --git a/library/Icinga/Web/Widget/Chart/InlinePie.php b/library/Icinga/Web/Widget/Chart/InlinePie.php index 4b4409e78..d22923131 100644 --- a/library/Icinga/Web/Widget/Chart/InlinePie.php +++ b/library/Icinga/Web/Widget/Chart/InlinePie.php @@ -7,7 +7,7 @@ namespace Icinga\Web\Widget\Chart; use Icinga\Web\Widget\AbstractWidget; use Icinga\Web\Url; use Icinga\Util\Format; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; /** * A SVG-PieChart intended to be displayed as a small icon next to labels, to offer a better visualization of the diff --git a/library/Icinga/Web/Widget/Dashboard.php b/library/Icinga/Web/Widget/Dashboard.php index a86204d87..0221e4744 100644 --- a/library/Icinga/Web/Widget/Dashboard.php +++ b/library/Icinga/Web/Widget/Dashboard.php @@ -5,7 +5,7 @@ namespace Icinga\Web\Widget; use Icinga\Application\Icinga; -use Icinga\Application\Config as IcingaConfig; +use Icinga\Application\Config; use Icinga\Exception\ConfigurationError; use Icinga\Exception\ProgrammingError; use Icinga\Web\Widget\Dashboard\Pane; @@ -26,7 +26,7 @@ class Dashboard extends AbstractWidget /** * The configuration containing information about this dashboard * - * @var IcingaConfig; + * @var Config; */ private $config; @@ -140,11 +140,11 @@ class Dashboard extends AbstractWidget /** * Populate this dashboard via the given configuration file * - * @param IcingaConfig $config The configuration file to populate this dashboard with + * @param Config $config The configuration file to populate this dashboard with * * @return self */ - public function readConfig(IcingaConfig $config) + public function readConfig(Config $config) { $this->config = $config; $this->panes = array(); diff --git a/library/Icinga/Web/Widget/Dashboard/Component.php b/library/Icinga/Web/Widget/Dashboard/Component.php index 7410c6210..6edf140f5 100644 --- a/library/Icinga/Web/Widget/Dashboard/Component.php +++ b/library/Icinga/Web/Widget/Dashboard/Component.php @@ -4,16 +4,12 @@ namespace Icinga\Web\Widget\Dashboard; -use Icinga\Exception\IcingaException; -use Icinga\Util\Dimension; +use Zend_Form_Element_Button; +use Icinga\Application\Config; use Icinga\Web\Form; use Icinga\Web\Url; use Icinga\Web\Widget\AbstractWidget; -use Icinga\Web\View; -use Zend_Config; -use Zend_Form_Element_Submit; -use Zend_Form_Element_Button; -use Exception; +use Icinga\Exception\IcingaException; /** * A dashboard pane component @@ -218,13 +214,13 @@ EOD; /** * Create a @see Component instance from the given Zend config, using the provided title * - * @param $title The title for this component - * @param Zend_Config $config The configuration defining url, parameters, height, width, etc. - * @param Pane $pane The pane this component belongs to + * @param $title The title for this component + * @param Config $config The configuration defining url, parameters, height, width, etc. + * @param Pane $pane The pane this component belongs to * - * @return Component A newly created Component for use in the Dashboard + * @return Component A newly created Component for use in the Dashboard */ - public static function fromIni($title, Zend_Config $config, Pane $pane) + public static function fromIni($title, Config $config, Pane $pane) { $height = null; $width = null; diff --git a/library/Icinga/Web/Widget/Dashboard/Pane.php b/library/Icinga/Web/Widget/Dashboard/Pane.php index 8c1e66bad..3b0e97c73 100644 --- a/library/Icinga/Web/Widget/Dashboard/Pane.php +++ b/library/Icinga/Web/Widget/Dashboard/Pane.php @@ -4,7 +4,7 @@ namespace Icinga\Web\Widget\Dashboard; -use Zend_Config; +use Icinga\Application\Config; use Icinga\Web\Widget\AbstractWidget; use Icinga\Exception\ProgrammingError; use Icinga\Exception\ConfigurationError; @@ -253,11 +253,11 @@ class Pane extends AbstractWidget * Create a new pane with the title $title from the given configuration * * @param $title The title for this pane - * @param Zend_Config $config The configuration to use for setup + * @param Config $config The configuration to use for setup * * @return Pane */ - public static function fromIni($title, Zend_Config $config) + public static function fromIni($title, Config $config) { $pane = new Pane($title); if ($config->get('title', false)) { diff --git a/library/Icinga/Web/Widget/Tab.php b/library/Icinga/Web/Widget/Tab.php index 92a1a9954..28c04e6ab 100644 --- a/library/Icinga/Web/Widget/Tab.php +++ b/library/Icinga/Web/Widget/Tab.php @@ -201,7 +201,7 @@ class Tab extends AbstractWidget $caption = $view->escape($this->title); if ($this->icon !== null) { - $caption = $view->img($this->icon, array('class' => 'icon')) . ' ' . $caption; + $caption = $view->img($this->icon, array('class' => 'icon')) . $caption; } if ($this->url !== null) { $this->url->overwriteParams($this->urlParams); diff --git a/library/Icinga/Web/Wizard.php b/library/Icinga/Web/Wizard.php new file mode 100644 index 000000000..faf950292 --- /dev/null +++ b/library/Icinga/Web/Wizard.php @@ -0,0 +1,522 @@ +init(); + } + + /** + * Run additional initialization routines + * + * Should be implemented by subclasses to add pages to the wizard. + */ + protected function init() + { + + } + + /** + * Return the pages being part of this wizard + * + * @return array + */ + public function getPages() + { + return $this->pages; + } + + /** + * Return the page with the given name + * + * @param string $name The name of the page to return + * + * @return null|Form The page or null in case there is no page with the given name + */ + public function getPage($name) + { + foreach ($this->getPages() as $page) { + if ($name === $page->getName()) { + return $page; + } + } + } + + /** + * Add a new page to this wizard + * + * @param Form $page The page to add to the wizard + * + * @return self + */ + public function addPage(Form $page) + { + $this->pages[] = $page; + return $this; + } + + /** + * Add multiple pages to this wizard + * + * @param array $pages The pages to add to the wizard + * + * @return self + */ + public function addPages(array $pages) + { + foreach ($pages as $page) { + $this->addPage($page); + } + + return $this; + } + + /** + * Assert that this wizard has any pages + * + * @throws LogicException In case this wizard has no pages + */ + protected function assertHasPages() + { + $pages = $this->getPages(); + if (count($pages) < 2) { + throw new LogicException("Although Chuck Norris can advance a wizard with less than two pages, you can't."); + } + } + + /** + * Return the current page of this wizard + * + * @return Form + * + * @throws LogicException In case the name of the current page currently being set is invalid + */ + public function getCurrentPage() + { + if ($this->currentPage === null) { + $this->assertHasPages(); + $pages = $this->getPages(); + $this->currentPage = $this->getSession()->get('current_page', $pages[0]->getName()); + } + + if (($page = $this->getPage($this->currentPage)) === null) { + throw new LogicException(sprintf('No page found with name "%s"', $this->currentPage)); + } + + return $page; + } + + /** + * Set the current page of this wizard + * + * @param Form $page The page to set as current page + * + * @return self + */ + public function setCurrentPage(Form $page) + { + $this->currentPage = $page->getName(); + $this->getSession()->set('current_page', $this->currentPage); + return $this; + } + + /** + * Setup the given page that is either going to be displayed or validated + * + * Implement this method in a subclass to populate default values and/or other data required to process the form. + * + * @param Form $page The page to setup + * @param Request $request The current request + */ + public function setupPage(Form $page, Request $request) + { + + } + + /** + * Process the given request using this wizard + * + * Validate the request data using the current page, update the wizard's + * position and redirect to the page's redirect url upon success. + * + * @param Request $request The request to be processed + * + * @return Request The request supposed to be processed + */ + public function handleRequest(Request $request = null) + { + $page = $this->getCurrentPage(); + + if ($request === null) { + $request = $page->getRequest(); + } + + $this->setupPage($page, $request); + $requestData = $page->getRequestData($request); + if ($page->wasSent($requestData)) { + if (($requestedPage = $this->getRequestedPage($requestData)) !== null) { + $isValid = false; + $direction = $this->getDirection($request); + if ($direction === static::FORWARD && $page->isValid($requestData)) { + $isValid = true; + if ($this->isLastPage($page)) { + $this->setIsFinished(); + } + } elseif ($direction === static::BACKWARD) { + $page->populate($requestData); + $isValid = true; + } + + if ($isValid) { + $pageData = & $this->getPageData(); + $pageData[$page->getName()] = $page->getValues(); + $this->setCurrentPage($this->getNewPage($requestedPage, $page)); + $page->getResponse()->redirectAndExit($page->getRedirectUrl()); + } + } else { + $page->isValidPartial($requestData); + } + } elseif (($pageData = $this->getPageData($page->getName())) !== null) { + $page->populate($pageData); + } + + return $request; + } + + /** + * Return the name of the requested page + * + * @param array $requestData The request's data + * + * @return null|string The name of the requested page or null in case no page has been requested + */ + protected function getRequestedPage(array $requestData) + { + if (isset($requestData[static::BTN_NEXT])) { + return $requestData[static::BTN_NEXT]; + } elseif (isset($requestData[static::BTN_PREV])) { + return $requestData[static::BTN_PREV]; + } + } + + /** + * Return the direction of this wizard using the given request + * + * @param Request $request The request to use + * + * @return int The direction @see Wizard::FORWARD @see Wizard::BACKWARD @see Wizard::NO_CHANGE + */ + protected function getDirection(Request $request = null) + { + $currentPage = $this->getCurrentPage(); + + if ($request === null) { + $request = $currentPage->getRequest(); + } + + $requestData = $currentPage->getRequestData($request); + if (isset($requestData[static::BTN_NEXT])) { + return static::FORWARD; + } elseif (isset($requestData[static::BTN_PREV])) { + return static::BACKWARD; + } + + return static::NO_CHANGE; + } + + /** + * Return the new page to set as current page + * + * Permission is checked by verifying that the requested page's previous page has page data available. + * The requested page is automatically permitted without any checks if the origin page is its previous + * page or one that occurs later in order. + * + * @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) + { + if (($page = $this->getPage($requestedPage)) !== null) { + $permitted = true; + + $pages = $this->getPages(); + if (($index = array_search($page, $pages, true)) > 0) { + $previousPage = $pages[$index - 1]; + if ($originPage === null || ($previousPage->getName() !== $originPage->getName() + && array_search($originPage, $pages, true) < $index)) + { + $permitted = $this->hasPageData($previousPage->getName()); + } + } + + if ($permitted) { + return $page; + } + } + + throw new InvalidArgumentException( + sprintf('"%s" is either an unknown page or one you are not permitted to view', $requestedPage) + ); + } + + /** + * Return whether the given page is this wizard's last page + * + * @param Form $page The page to check + * + * @return bool + */ + protected function isLastPage(Form $page) + { + $pages = $this->getPages(); + return $page->getName() === end($pages)->getName(); + } + + /** + * Set whether this wizard has been completed + * + * @param bool $state Whether this wizard has been completed + * + * @return self + */ + public function setIsFinished($state = true) + { + $this->getSession()->set('isFinished', $state); + return $this; + } + + /** + * Return whether this wizard has been completed + * + * @return bool + */ + public function isFinished() + { + return $this->getSession()->get('isFinished', false); + } + + /** + * Return the overall page data or one for a particular page + * + * Note that this method returns by reference so in order to update the + * returned array set this method's return value also by reference. + * + * @param string $pageName The page for which to return the data + * + * @return array + */ + public function & getPageData($pageName = null) + { + $session = $this->getSession(); + + if (false === isset($session->page_data)) { + $session->page_data = array(); + } + + $pageData = & $session->getByRef('page_data'); + if ($pageName !== null) { + $data = null; + if (isset($pageData[$pageName])) { + $data = & $pageData[$pageName]; + } + + return $data; + } + + return $pageData; + } + + /** + * Return whether there is any data for the given page + * + * @param string $pageName The name of the page to check + * + * @return bool + */ + public function hasPageData($pageName) + { + return $this->getPageData($pageName) !== null; + } + + /** + * Return a session to be used by this wizard + * + * @return SessionNamespace + */ + public function getSession() + { + return Session::getSession()->getNamespace(get_class($this)); + } + + /** + * Clear the session being used by this wizard + */ + public function clearSession() + { + $this->getSession()->clear(); + } + + /** + * 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) + { + $pages = $this->getPages(); + $index = array_search($page, $pages, true); + if ($index === 0) { + $page->addElement( + 'button', + static::BTN_NEXT, + array( + 'type' => 'submit', + 'value' => $pages[1]->getName(), + 'label' => t('Next'), + 'decorators' => array('ViewHelper') + ) + ); + } elseif ($index < count($pages) - 1) { + $page->addElement( + 'button', + static::BTN_PREV, + array( + 'type' => 'submit', + 'value' => $pages[$index - 1]->getName(), + 'label' => t('Back'), + 'decorators' => array('ViewHelper') + ) + ); + $page->addElement( + 'button', + static::BTN_NEXT, + array( + 'type' => 'submit', + 'value' => $pages[$index + 1]->getName(), + 'label' => t('Next'), + 'decorators' => array('ViewHelper') + ) + ); + } else { + $page->addElement( + 'button', + static::BTN_PREV, + array( + 'type' => 'submit', + 'value' => $pages[$index - 1]->getName(), + 'label' => t('Back'), + 'decorators' => array('ViewHelper') + ) + ); + $page->addElement( + 'button', + static::BTN_NEXT, + array( + 'type' => 'submit', + 'value' => $page->getName(), + 'label' => t('Finish'), + 'decorators' => array('ViewHelper') + ) + ); + } + + $page->addDisplayGroup( + array(static::BTN_PREV, static::BTN_NEXT), + 'buttons', + array( + 'decorators' => array( + 'FormElements', + new ElementDoubler(array( + 'double' => static::BTN_NEXT, + 'condition' => static::BTN_PREV, + 'placement' => ElementDoubler::PREPEND, + 'attributes' => array('tabindex' => -1, 'class' => 'double') + )), + array('HtmlTag', array('tag' => 'div', 'class' => 'buttons')) + ) + ) + ); + } + + /** + * Return the current page of this wizard with appropriate buttons being added + * + * @return Form + */ + public function getForm() + { + $form = $this->getCurrentPage(); + $form->create(); // Make sure that buttons are displayed at the very bottom + $this->addButtons($form); + return $form; + } + + /** + * Return the current page of this wizard rendered as HTML + * + * @return string + */ + public function __toString() + { + return (string) $this->getForm(); + } +} diff --git a/modules/monitoring/application/controllers/CommandController.php b/modules/monitoring/application/controllers/CommandController.php index 1aae9de20..6f45a388b 100644 --- a/modules/monitoring/application/controllers/CommandController.php +++ b/modules/monitoring/application/controllers/CommandController.php @@ -4,22 +4,19 @@ use Icinga\Application\Icinga; use Icinga\Application\Config; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Module\Monitoring\Form\Command\DisableNotificationWithExpireForm; use Icinga\Module\Monitoring\Form\Command\SingleArgumentCommandForm; use Icinga\Web\Form; use Icinga\Web\Url; -use Icinga\Data\Filter\Filter; use Icinga\Web\Notification; use Icinga\Module\Monitoring\Controller; use Icinga\Protocol\Commandpipe\CommandPipe; use Icinga\Exception\ConfigurationError; use Icinga\Exception\MissingParameterException; -use Icinga\Module\Monitoring\Backend; use Icinga\Module\Monitoring\Form\Command\AcknowledgeForm; use Icinga\Module\Monitoring\Form\Command\CommentForm; use Icinga\Module\Monitoring\Form\Command\CommandForm; -use Icinga\Module\Monitoring\Form\Command\CommandWithIdentifierForm; use Icinga\Module\Monitoring\Form\Command\CustomNotificationForm; use Icinga\Module\Monitoring\Form\Command\DelayNotificationForm; use Icinga\Module\Monitoring\Form\Command\RescheduleNextCheckForm; diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php index e537496f9..b60ae0a31 100644 --- a/modules/monitoring/application/controllers/ListController.php +++ b/modules/monitoring/application/controllers/ListController.php @@ -296,7 +296,7 @@ class Monitoring_ListController extends Controller ->order('downtime_scheduled_start', 'DESC'); $this->applyFilters($query); - $this->view->downtimes = $query->paginate(); + $this->setupSortControl(array( 'downtime_is_in_effect' => $this->translate('Is In Effect'), 'downtime_host' => $this->translate('Host / Service'), @@ -308,6 +308,8 @@ class Monitoring_ListController extends Controller 'downtime_scheduled_end' => $this->translate('Scheduled End'), 'downtime_duration' => $this->translate('Duration'), )); + + $this->view->downtimes = $query->paginate(); $this->view->delDowntimeForm = new DeleteDowntimeCommandForm(); } @@ -570,12 +572,12 @@ class Monitoring_ListController extends Controller $this->view->history = $query->paginate(); } - public function servicematrixAction() + public function servicegridAction() { if ($url = $this->hasBetterUrl()) { return $this->redirectNow($url); } - $this->addTitleTab('servicematrix'); + $this->addTitleTab('servicegrid', $this->translate('Service Grid')); $this->setAutorefreshInterval(15); $query = $this->backend->select()->from('serviceStatus', array( 'host_name', diff --git a/modules/monitoring/application/controllers/TimelineController.php b/modules/monitoring/application/controllers/TimelineController.php index 061765f1c..e57943aa6 100644 --- a/modules/monitoring/application/controllers/TimelineController.php +++ b/modules/monitoring/application/controllers/TimelineController.php @@ -4,16 +4,13 @@ use \DateTime; use \DateInterval; -use \Zend_Config; use Icinga\Web\Url; use Icinga\Util\Format; -use Icinga\Application\Config; use Icinga\Util\DateTimeFactory; use Icinga\Module\Monitoring\Controller; use Icinga\Module\Monitoring\Timeline\TimeLine; use Icinga\Module\Monitoring\Timeline\TimeRange; use Icinga\Module\Monitoring\Web\Widget\SelectBox; -use Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView; class Monitoring_TimelineController extends Controller { @@ -257,22 +254,6 @@ class Monitoring_TimelineController extends Controller ); } - /** - * Get the application's global configuration or an empty one - * - * @return Zend_Config - */ - private function getGlobalConfiguration() - { - $globalConfig = Config::app()->global; - - if ($globalConfig === null) { - $globalConfig = new Zend_Config(array()); - } - - return $globalConfig; - } - /** * Get the user's preferred time format or the application's default * diff --git a/modules/monitoring/application/forms/Command/CommandForm.php b/modules/monitoring/application/forms/Command/CommandForm.php index 0a7cd19f9..da797e60c 100644 --- a/modules/monitoring/application/forms/Command/CommandForm.php +++ b/modules/monitoring/application/forms/Command/CommandForm.php @@ -4,7 +4,7 @@ namespace Icinga\Module\Monitoring\Form\Command; -use Icinga\Module\Monitoring\Backend; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; use Icinga\Module\Monitoring\Command\Transport\CommandTransport; use Icinga\Web\Form; use Icinga\Web\Request; @@ -24,11 +24,11 @@ abstract class CommandForm extends Form /** * Set the monitoring backend * - * @param Backend $backend + * @param MonitoringBackend $backend * * @return $this */ - public function setBackend(Backend $backend) + public function setBackend(MonitoringBackend $backend) { $this->backend = $backend; return $this; diff --git a/modules/monitoring/application/forms/Config/BackendConfigForm.php b/modules/monitoring/application/forms/Config/BackendConfigForm.php index 472046636..e18e882ca 100644 --- a/modules/monitoring/application/forms/Config/BackendConfigForm.php +++ b/modules/monitoring/application/forms/Config/BackendConfigForm.php @@ -225,15 +225,31 @@ class BackendConfigForm extends ConfigForm 'value' => $resourceType ) ); - $this->addElement( + + $resourceElement = $this->createElement( 'select', 'resource', array( 'required' => true, 'label' => mt('monitoring', 'Resource'), 'description' => mt('monitoring', 'The resource to use'), - 'multiOptions' => $this->resources[$resourceType] + 'multiOptions' => $this->resources[$resourceType], + 'autosubmit' => true ) ); + + $resourceName = (isset($formData['resource'])) ? $formData['resource'] : $this->getValue('resource'); + if ($resourceElement) { + $resourceElement->getDecorator('Description')->setEscape(false); + $link = sprintf( + '%s', + $this->getView()->href('/icingaweb/config/editresource', array('resource' => $resourceName)), + mt('monitoring', 'Show resource configuration') + ); + $resourceElement->setDescription($resourceElement->getDescription() . ' (' . $link . ')'); + } + + $this->addElement($resourceElement); + } } diff --git a/modules/monitoring/application/forms/Config/SecurityConfigForm.php b/modules/monitoring/application/forms/Config/SecurityConfigForm.php index a57e82702..c7e8d7efb 100644 --- a/modules/monitoring/application/forms/Config/SecurityConfigForm.php +++ b/modules/monitoring/application/forms/Config/SecurityConfigForm.php @@ -55,7 +55,8 @@ class SecurityConfigForm extends ConfigForm 'text', 'protected_customvars', array( - 'required' => true, + 'allowEmpty' => true, + 'value' => '*pw*,*pass*,community', 'label' => mt('monitoring', 'Protected Custom Variables'), 'description' => mt('monitoring', 'Comma separated case insensitive list of protected custom variables.' diff --git a/modules/monitoring/application/forms/Setup/BackendPage.php b/modules/monitoring/application/forms/Setup/BackendPage.php new file mode 100644 index 000000000..097d2586b --- /dev/null +++ b/modules/monitoring/application/forms/Setup/BackendPage.php @@ -0,0 +1,72 @@ +setName('setup_monitoring_backend'); + } + + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('monitoring', 'Monitoring Backend', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'Please configure below how Icinga Web 2 should retrieve monitoring information.' + ) + ) + ) + ); + + $this->addElement( + 'text', + 'name', + array( + 'required' => true, + 'value' => 'icinga', + 'label' => mt('monitoring', 'Backend Name'), + 'description' => mt('monitoring', 'The identifier of this backend') + ) + ); + + $resourceTypes = array(); + if (Platform::extensionLoaded('mysql') || Platform::extensionLoaded('pgsql')) { + $resourceTypes['ido'] = 'IDO'; + } + $resourceTypes['livestatus'] = 'Livestatus'; + + $this->addElement( + 'select', + 'type', + array( + 'required' => true, + 'label' => mt('monitoring', 'Backend Type'), + 'description' => mt('monitoring', 'The data source used for retrieving monitoring information'), + 'multiOptions' => $resourceTypes + ) + ); + } +} diff --git a/modules/monitoring/application/forms/Setup/IdoResourcePage.php b/modules/monitoring/application/forms/Setup/IdoResourcePage.php new file mode 100644 index 000000000..3e844027c --- /dev/null +++ b/modules/monitoring/application/forms/Setup/IdoResourcePage.php @@ -0,0 +1,102 @@ +setName('setup_monitoring_ido'); + } + + public function createElements(array $formData) + { + $this->addElement( + 'hidden', + 'type', + array( + 'required' => true, + 'value' => 'db' + ) + ); + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('monitoring', 'Monitoring IDO Resource', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'Please fill out the connection details below to access' + . ' the IDO database of your monitoring environment.' + ) + ) + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + $this->addSkipValidationCheckbox(); + } else { + $this->addElement( + 'hidden', + 'skip_validation', + array( + 'required' => true, + 'value' => 0 + ) + ); + } + + $livestatusResourceForm = new DbResourceForm(); + $this->addElements($livestatusResourceForm->createElements($formData)->getElements()); + $this->getElement('name')->setValue('icinga_ido'); + } + + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) { + if (false === DbResourceForm::isValidResource($this)) { + $this->addSkipValidationCheckbox(); + return false; + } + } + + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the connection validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'required' => true, + 'label' => t('Skip Validation'), + 'description' => t('Check this to not to validate connectivity with the given database server') + ) + ); + } +} diff --git a/modules/monitoring/application/forms/Setup/InstancePage.php b/modules/monitoring/application/forms/Setup/InstancePage.php new file mode 100644 index 000000000..7398dae1c --- /dev/null +++ b/modules/monitoring/application/forms/Setup/InstancePage.php @@ -0,0 +1,53 @@ +setName('setup_monitoring_instance'); + } + + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('monitoring', 'Monitoring Instance', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'Please define the settings specific to your monitoring instance below.' + ) + ) + ) + ); + + if (isset($formData['host'])) { + $formData['type'] = 'remote'; // This is necessary as the type element gets ignored by Form::getValues() + } + + $instanceConfigForm = new InstanceConfigForm(); + $instanceConfigForm->createElements($formData); + $this->addElements($instanceConfigForm->getElements()); + $this->getElement('name')->setValue('icinga'); + } +} diff --git a/modules/monitoring/application/forms/Setup/LivestatusResourcePage.php b/modules/monitoring/application/forms/Setup/LivestatusResourcePage.php new file mode 100644 index 000000000..9ae36aebf --- /dev/null +++ b/modules/monitoring/application/forms/Setup/LivestatusResourcePage.php @@ -0,0 +1,102 @@ +setName('setup_monitoring_livestatus'); + } + + public function createElements(array $formData) + { + $this->addElement( + 'hidden', + 'type', + array( + 'required' => true, + 'value' => 'livestatus' + ) + ); + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('monitoring', 'Monitoring Livestatus Resource', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'Please fill out the connection details below to access the Livestatus' + . ' socket interface for your monitoring environment.' + ) + ) + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + $this->addSkipValidationCheckbox(); + } else { + $this->addElement( + 'hidden', + 'skip_validation', + array( + 'required' => true, + 'value' => 0 + ) + ); + } + + $livestatusResourceForm = new LivestatusResourceForm(); + $this->addElements($livestatusResourceForm->createElements($formData)->getElements()); + $this->getElement('name')->setValue('icinga_livestatus'); + } + + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) { + if (false === LivestatusResourceForm::isValidResource($this)) { + $this->addSkipValidationCheckbox(); + return false; + } + } + + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the connection validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'required' => true, + 'label' => t('Skip Validation'), + 'description' => t('Check this to not to validate connectivity with the given Livestatus socket') + ) + ); + } +} diff --git a/modules/monitoring/application/forms/Setup/SecurityPage.php b/modules/monitoring/application/forms/Setup/SecurityPage.php new file mode 100644 index 000000000..70c90c8a3 --- /dev/null +++ b/modules/monitoring/application/forms/Setup/SecurityPage.php @@ -0,0 +1,48 @@ +setName('setup_monitoring_security'); + } + + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('monitoring', 'Monitoring Security', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'To protect your monitoring environment against prying eyes please fill out the settings below.' + ) + ) + ) + ); + + $securityConfigForm = new SecurityConfigForm(); + $securityConfigForm->createElements($formData); + $this->addElements($securityConfigForm->getElements()); + } +} diff --git a/modules/monitoring/application/forms/Setup/WelcomePage.php b/modules/monitoring/application/forms/Setup/WelcomePage.php new file mode 100644 index 000000000..be1f35364 --- /dev/null +++ b/modules/monitoring/application/forms/Setup/WelcomePage.php @@ -0,0 +1,68 @@ +setName('setup_monitoring_welcome'); + } + + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'welcome', + array( + 'value' => mt( + 'monitoring', + 'Welcome to the configuration of the monitoring module for Icinga Web 2!' + ), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + + $this->addElement( + new Note( + 'core_hint', + array( + 'value' => mt('monitoring', 'This is the core module for Icinga Web 2.') + ) + ) + ); + + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'It offers various status and reporting views with powerful filter capabilities that allow' + . ' you to keep track of the most important events in your monitoring environment.' + ) + ) + ) + ); + + $this->addDisplayGroup( + array('core_hint', 'description'), + 'info', + array( + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'div', 'class' => 'info')) + ) + ) + ); + } +} diff --git a/modules/monitoring/application/views/helpers/Customvar.php b/modules/monitoring/application/views/helpers/Customvar.php new file mode 100644 index 000000000..699c6a727 --- /dev/null +++ b/modules/monitoring/application/views/helpers/Customvar.php @@ -0,0 +1,55 @@ +view->escape((string) $struct); + } elseif (is_array($struct)) { + return $this->renderArray($struct); + } elseif (is_object($struct)) { + return $this->renderObject($struct); + } + } + + protected function renderArray($array) + { + if (empty($array)) { + return '[]'; + } + $out = "\n"; + } + + protected function renderObject($object) + { + if (empty($object)) { + return '{}'; + } + $out = "{}"; + } + +} + diff --git a/modules/monitoring/application/views/helpers/MonitoringState.php b/modules/monitoring/application/views/helpers/MonitoringState.php index 871e8ad08..4db1a7a91 100644 --- a/modules/monitoring/application/views/helpers/MonitoringState.php +++ b/modules/monitoring/application/views/helpers/MonitoringState.php @@ -95,8 +95,11 @@ class Zend_View_Helper_MonitoringState extends Zend_View_Helper_Abstract */ public function getStateTitle($object, $type) { - return strtoupper($this->monitoringState($object, $type)) - . ' since ' - . date('Y-m-d H:i:s', $object->{$type.'_last_state_change'}); + return sprintf( + '%s %s %s', + $this->view->translate(strtoupper($this->monitoringState($object, $type))), + $this->view->translate('since'), + date('Y-m-d H:i:s', $object->{$type.'_last_state_change'}) + ); } } diff --git a/modules/monitoring/application/views/scripts/list/downtimes.phtml b/modules/monitoring/application/views/scripts/list/downtimes.phtml index b0755a974..25a2529f6 100644 --- a/modules/monitoring/application/views/scripts/list/downtimes.phtml +++ b/modules/monitoring/application/views/scripts/list/downtimes.phtml @@ -3,6 +3,9 @@ tabs->render($this); ?>
translate('Sort by'); ?> sortControl->render($this); ?> + filterEditor): ?> + filterPreview ?> +
widget('limiter', array('url' => $this->url, 'max' => $downtimes->count())); ?> paginationControl($downtimes, null, null, array('preserve' => $this->preserve)); ?> @@ -10,6 +13,7 @@
+filterEditor ?> translate('No downtimes matching the filter'); ?>
diff --git a/modules/monitoring/application/views/scripts/list/servicegrid.phtml b/modules/monitoring/application/views/scripts/list/servicegrid.phtml new file mode 100644 index 000000000..2bbfb343f --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/servicegrid.phtml @@ -0,0 +1,77 @@ +compact): ?> +
+ tabs; ?> +
+ translate('Sort by'); ?> sortControl; ?> +
+
+ +
+ +pivot->toArray(); +$hostFilter = '(' . implode('|', array_keys($pivotData)) . ')'; +?> + + + translate('No Services matching the filter'); ?> + + + $serviceStates): ?> + + + + + + + + + + + + + + + + + + + + + + +
partial( + 'joystickPagination.phtml', + 'default', + array( + 'xAxisPaginator' => $horizontalPaginator, + 'yAxisPaginator' => $verticalPaginator + ) + ); ?> + +
+ + + + ·
+
diff --git a/modules/monitoring/application/views/scripts/list/servicematrix.phtml b/modules/monitoring/application/views/scripts/list/servicematrix.phtml deleted file mode 100644 index 62676a2d2..000000000 --- a/modules/monitoring/application/views/scripts/list/servicematrix.phtml +++ /dev/null @@ -1,84 +0,0 @@ -compact): ?> -
- tabs; ?> -
- translate('Sort by') ?> sortControl ?> -
- partial( - 'pivottablePagination.phtml', - 'default', - array( - 'xAxisPaginator' => $this->horizontalPaginator, - 'yAxisPaginator' => $this->verticalPaginator - ) - ); ?> -
- -
- -pivot->toArray(); -$hostFilter = '(' . implode('|', array_keys($pivotData)) . ')'; -?> - - - translate('No Services matching the filter'); ?> - - - $serviceStates): ?> - - - - - - - - - - - - - - - - - - - - - - - -
  - -
- - - - ·
-
diff --git a/modules/monitoring/application/views/scripts/show/components/customvars.phtml b/modules/monitoring/application/views/scripts/show/components/customvars.phtml index d89260e95..aa2e479e4 100644 --- a/modules/monitoring/application/views/scripts/show/components/customvars.phtml +++ b/modules/monitoring/application/views/scripts/show/components/customvars.phtml @@ -1,8 +1,11 @@ customvars as $name => $value) { + printf( - "%s%s\n", + '%s%s' . "\n", $this->escape($name), - $this->escape($value) + $this->customvar($value) ); } + diff --git a/modules/monitoring/bin/action/list.inc.php b/modules/monitoring/bin/action/list.inc.php deleted file mode 100644 index 01fd35131..000000000 --- a/modules/monitoring/bin/action/list.inc.php +++ /dev/null @@ -1,123 +0,0 @@ -shift('backend')); - -$query = $backend->select()->from('status', array( - 'host_name', - 'host_state', - 'host_output', - 'host_acknowledged', - 'host_in_downtime', - 'service_description', - 'service_state', - 'service_acknowledged', - 'service_in_downtime', - 'service_handled', - 'service_output', - 'service_last_state_change' -))->order('service_last_state_change ASC'); - -$endless = $params->shift('endless'); -$query->applyFilters($params->getParams()); -$host_colors = array( - 0 => '2', // UP - 1 => '1', // DOWN - 2 => '3', // UNREACH (brown) - 99 => '0', // PEND -); -$host_states = array( - 0 => 'UP', // UP - 1 => 'DOWN', // DOWN - 2 => 'UNREACHABLE', // UNREACH (brown) - 99 => 'PENDING', // PEND -); -$service_colors = array( - 0 => '2', // OK - 1 => '3', // WARN - 2 => '1', // CRIT - 3 => '5', // UNKN - 99 => '0', // PEND -); -$service_states = array( - 0 => 'OK', // OK - 1 => 'WARNING', // WARN - 2 => 'CRITICAL', // CRIT - 3 => 'UNKNOWN', // UNKN - 99 => 'PENDING', // PEND -); - -$finished = false; -while (! $finished) { -$out = ''; -$last_host = null; - -foreach ($query->fetchAll() as $key => $row) { - $host_extra = array(); - if ($row->host_in_downtime) { - $host_extra[] = 'DOWNTIME'; - } - if ($row->host_acknowledged) { - $host_extra[] = 'ACK'; - } - if (empty($host_extra)) { - $host_extra = ''; - } else { - $host_extra = " \033[34;1m[" . implode(',', $host_extra) . "]\033[0m"; - } - - $service_extra = array(); - if ($row->service_in_downtime) { - $service_extra[] = 'DOWNTIME'; - } - if ($row->service_acknowledged) { - $service_extra[] = 'ACK'; - } - if (empty($service_extra)) { - $service_extra = ''; - } else { - $service_extra = " \033[34;52;1m[" . implode(',', $service_extra) . "]\033[0m"; - } - - if ($row->host_name !== $last_host) { - $out .= sprintf( - "\n\033[01;37;4%dm %-5s \033[0m \033[30;1m%s\033[0m%s: %s\n", - $host_colors[$row->host_state], - substr($host_states[$row->host_state], 0, 5), - $row->host_name, - $host_extra, - $row->host_output - ); - } - $last_host = $row->host_name; - $out .= sprintf( - "\033[01;37;4%dm \033[01;37;4%dm %4s \033[0m %s%s since %s: %s\n", - $host_colors[$row->host_state], - $service_colors[$row->service_state], - substr($service_states[$row->service_state] . ' ', 0, 4), - $row->service_description, - $service_extra, - Format::timeSince($row->service_last_state_change), - preg_replace('/\n/', sprintf( - "\n\033[01;37;4%dm \033[01;37;4%dm \033[0m ", - $host_colors[$row->host_state], - $service_colors[$row->service_state] - - ), substr(wordwrap(str_repeat(' ', 30) . preg_replace('~\@{3,}~', '@@@', $row->service_output), 72), 30)) - ); -} - -$out .= "\n"; - - if ($endless) { - echo "\033[2J\033[1;1H\033[1S" . $out; - sleep(3); - } else { - echo $out; - $finished = true; - } -} diff --git a/modules/monitoring/configuration.php b/modules/monitoring/configuration.php index edcd37a24..848d213bb 100644 --- a/modules/monitoring/configuration.php +++ b/modules/monitoring/configuration.php @@ -18,6 +18,7 @@ $this->provideConfigTab('security', array( 'title' => 'Security', 'url' => 'config/security' )); +$this->provideSetupWizard('Icinga\Module\Monitoring\MonitoringWizard'); /* * Available Search Urls @@ -74,8 +75,8 @@ $section->add($this->translate('Services'), array( 'url' => 'monitoring/list/services', 'priority' => 50 )); -$section->add($this->translate('Servicematrix'), array( - 'url' => 'monitoring/list/servicematrix?service_problem=1', +$section->add($this->translate('Service Grid'), array( + 'url' => 'monitoring/list/servicegrid?service_problem=1', 'priority' => 51 )); $section->add($this->translate('Servicegroups'), array( diff --git a/modules/monitoring/library/Monitoring/Backend.php b/modules/monitoring/library/Monitoring/Backend.php index f6aa6b447..e3672dcd5 100644 --- a/modules/monitoring/library/Monitoring/Backend.php +++ b/modules/monitoring/library/Monitoring/Backend.php @@ -1,186 +1,11 @@ resource = $resource; - $this->type = $type; - } - - // Temporary workaround, we have no way to know our name - protected function setName($name) - { - $this->name = $name; - } - - public function getName() - { - return $this->name; - } - - /** - * Create a backend - * - * @param string $backendName Name of the backend or null for creating the default backend which is the first INI - * configuration entry not being disabled - * - * @return Backend - * @throws ConfigurationError When no backend has been configured or all backends are disabled or the - * configuration for the requested backend does either not exist or it's disabled - */ - public static function createBackend($backendName = null) - { - $config = IcingaConfig::module('monitoring', 'backends'); - if ($config->count() === 0) { - throw new ConfigurationError(mt('monitoring', 'No backend has been configured')); - } - if ($backendName !== null) { - $backendConfig = $config->get($backendName); - if ($backendConfig === null) { - throw new ConfigurationError('No configuration for backend %s', $backendName); - } - if ((bool) $backendConfig->get('disabled', false) === true) { - throw new ConfigurationError( - mt('monitoring', 'Configuration for backend %s available but backend is disabled'), - $backendName - ); - } - } else { - foreach ($config as $name => $backendConfig) { - if ((bool) $backendConfig->get('disabled', false) === false) { - $backendName = $name; - break; - } - } - if ($backendName === null) { - throw new ConfigurationError(mt('monitoring', 'All backends are disabled')); - } - } - $resource = ResourceFactory::create($backendConfig->resource); - if ($backendConfig->type === 'ido' && $resource->getDbType() !== 'oracle') { - // TODO(el): The resource should set the table prefix - $resource->setTablePrefix('icinga_'); - } - $backend = new Backend($resource, $backendConfig->type); - $backend->setName($backendName); - return $backend; - } - -public function getResource() -{ - return $this->resource; -} - - /** - * Backend entry point - * - * @return self - */ - public function select() - { - return $this; - } - - /** - * Create a data view to fetch data from - * - * @param string $viewName - * @param array $columns - * - * @return DataView - */ - public function from($viewName, array $columns = null) - { - $viewClass = $this->resolveDataViewName($viewName); - return new $viewClass($this, $columns); - } - - /** - * View name to class name resolution - * - * @param string $viewName - * - * @return string - * @throws ProgrammingError When the view does not exist - */ - protected function resolveDataViewName($viewName) - { - $viewClass = '\\Icinga\\Module\\Monitoring\\DataView\\' . ucfirst($viewName); - if (!class_exists($viewClass)) { - throw new ProgrammingError( - 'DataView %s does not exist', - ucfirst($viewName) - ); - } - return $viewClass; - } - - public function getQueryClass($name) - { - return $this->resolveQueryName($name); - } - - /** - * Query name to class name resolution - * - * @param string $queryName - * - * @return string - * @throws ProgrammingError When the query does not exist for this backend - */ - protected function resolveQueryName($queryName) - { - $queryClass = '\\Icinga\\Module\\Monitoring\\Backend\\' - . ucfirst($this->type) - . '\\Query\\' - . ucfirst($queryName) - . 'Query'; - if (!class_exists($queryClass)) { - throw new ProgrammingError( - 'Query "%s" does not exist for backend %s', - ucfirst($queryName), - ucfirst($this->type) - ); - } - return $queryClass; - } } diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/IdoBackend.php b/modules/monitoring/library/Monitoring/Backend/Ido/IdoBackend.php new file mode 100644 index 000000000..17bfb9f6a --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/IdoBackend.php @@ -0,0 +1,9 @@ + array( 'varname' => 'cvs.varname', 'varvalue' => 'cvs.varvalue', + 'is_json' => 'cvs.is_json', ), 'objects' => array( 'host' => 'cvo.name1 COLLATE latin1_general_ci', @@ -37,6 +38,10 @@ class CustomvarQuery extends IdoQuery protected function joinBaseTables() { + if (version_compare($this->getIdoVersion(), '1.12.0', '<')) { + $this->columnMap['customvars']['is_json'] = '(0)'; + } + $this->select->from( array('cvs' => $this->prefix . 'customvariablestatus'), array() diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php index 2393c6ed9..a53fd18e9 100644 --- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php @@ -16,6 +16,7 @@ class DowntimeQuery extends IdoQuery protected $columnMap = array( 'downtime' => array( 'downtime_author' => 'sd.author_name', + 'author' => 'sd.author_name', 'downtime_comment' => 'sd.comment_data', 'downtime_entry_time' => 'UNIX_TIMESTAMP(sd.entry_time)', 'downtime_is_fixed' => 'sd.is_fixed', diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php index ab1947f7e..5a6bce41f 100644 --- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php @@ -4,7 +4,7 @@ namespace Icinga\Module\Monitoring\Backend\Ido\Query; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Zend_Db_Select; class GroupSummaryQuery extends IdoQuery diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php index 06472b44d..4682487d2 100644 --- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php @@ -5,7 +5,7 @@ namespace Icinga\Module\Monitoring\Backend\Ido\Query; use Icinga\Exception\IcingaException; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Data\Db\DbQuery; use Icinga\Exception\ProgrammingError; use Icinga\Application\Icinga; @@ -655,7 +655,6 @@ abstract class IdoQuery extends DbQuery ); if ($session !== null) { $session->version = self::$idoVersion; - $session->write(); // <- WHY? I don't want to care about this! } } return self::$idoVersion; diff --git a/modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php b/modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php new file mode 100644 index 000000000..11a8fb699 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php @@ -0,0 +1,331 @@ +name = $name; + $this->config = $config; + } + + /** + * Get a backend instance + * + * You may ask for a specific backend name or get the default one otherwise + * + * @param string $name Backend name + * + * @return MonitoringBackend + */ + public static function instance($name = null) + { + if (! array_key_exists($name, self::$instances)) { + + list($foundName, $config) = static::loadConfig($name); + $type = $config->get('type'); + $class = implode( + '\\', + array( + __NAMESPACE__, + ucfirst($type), + ucfirst($type) . 'Backend' + ) + ); + + if (!class_exists($class)) { + throw new ConfigurationError( + mt('monitoring', 'There is no "%s" monitoring backend'), + $class + ); + } + + self::$instances[$name] = new $class($foundName, $config); + if ($name === null) { + self::$instances[$foundName] = self::$instances[$name]; + } + } + + return self::$instances[$name]; + } + + /** + * Clear all cached instances. Mostly for testing purposes. + */ + public static function clearInstances() + { + self::$instances = array(); + } + + /** + * Whether this backend is of a specific type + * + * @param string $type Backend type + * + * @return boolean + */ + public function is($type) + { + return $this->getType() === $type; + } + + /** + * Get the configured name of this backend + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the backend type name + * + * @return string + */ + public function getType() + { + if ($this->type === null) { + $parts = preg_split('~\\\~', get_class($this)); + $class = array_pop($parts); + if (substr($class, -7) === 'Backend') { + $this->type = lcfirst(substr($class, 0, -7)); + } else { + throw new ProgrammingError( + '%s is not a valid monitoring backend class name', + $class + ); + } + } + return $this->type; + } + + /** + * Return the configuration for the first enabled or the given backend + */ + protected static function loadConfig($name = null) + { + $backends = Config::module('monitoring', 'backends'); + + if ($name === null) { + + $count = 0; + + foreach ($backends as $name => $config) { + $count++; + if ((bool) $config->get('disabled', false) === false) { + return array($name, $config); + } + } + + if ($count === 0) { + $message = mt('monitoring', 'No backend has been configured'); + } else { + $message = mt('monitoring', 'All backends are disabled'); + } + + throw new ConfigurationError($message); + + } else { + + $config = $backends->get($name); + + if ($config === null) { + throw new ConfigurationError( + mt('monitoring', 'No configuration for backend %s'), + $name + ); + } + + if ((bool) $config->get('disabled', false) === true) { + throw new ConfigurationError( + mt('monitoring', 'Configuration for backend %s is disabled'), + $name + ); + } + + return array($name, $config); + } + } + + /** + * Create a backend + * + * @deprecated + * + * @param string $backendName Name of the backend or null for creating the default backend which is the first INI + * configuration entry not being disabled + * + * @return Backend + * @throws ConfigurationError When no backend has been configured or all backends are disabled or the + * configuration for the requested backend does either not exist or it's disabled + */ + public static function createBackend($name = null) + { + return self::instance($name); + } + + /** + * Get this backend's internal resource + * + * @return mixed + */ + public function getResource() + { + if ($this->resource === null) { + $this->resource = ResourceFactory::create($this->config->get('resource')); + if ($this->is('ido') && $this->resource->getDbType() !== 'oracle') { + // TODO(el): The resource should set the table prefix + $this->resource->setTablePrefix('icinga_'); + } + } + return $this->resource; + } + + /** + * Backend entry point + * + * @return self + */ + public function select() + { + return $this; + } + + /** + * Create a data view to fetch data from + * + * @param string $name + * @param array $columns + * + * @return DataView + */ + public function from($name, array $columns = null) + { + $class = $this->buildViewClassName($name); + return new $class($this, $columns); + } + + /** + * View name to class name resolution + * + * @param string $viewName + * + * @return string + * @throws ProgrammingError When the view does not exist + */ + protected function buildViewClassName($view) + { + $class = '\\Icinga\\Module\\Monitoring\\DataView\\' . ucfirst($view); + if (!class_exists($class)) { + throw new ProgrammingError( + 'DataView %s does not exist', + ucfirst($view) + ); + } + return $class; + } + + /** + * Get a specific query class instance + * + * @param string $name Query name + * @param array $columns Optional column list + * + * @return Icinga\Data\QueryInterface + */ + public function query($name, $columns = null) + { + $class = $this->buildQueryClassName($name); + + if (!class_exists($class)) { + throw new ProgrammingError( + 'Query "%s" does not exist for backend %s', + $name, + $this->getType() + ); + } + + return new $class($this->getResource(), $columns); + } + + /** + * Whether this backend supports the given query + * + * @param string $name Query name to check for + * + * @return bool + */ + public function hasQuery($name) + { + return class_exists($this->buildQueryClassName($name)); + } + + /** + * Query name to class name resolution + * + * @param string $query + * + * @return string + * @throws ProgrammingError When the query does not exist for this backend + */ + protected function buildQueryClassName($query) + { + $parts = preg_split('~\\\~', get_class($this)); + array_pop($parts); + array_push($parts, 'Query', ucfirst($query) . 'Query'); + return implode('\\', $parts); + } +} diff --git a/modules/monitoring/library/Monitoring/BackendStep.php b/modules/monitoring/library/Monitoring/BackendStep.php new file mode 100644 index 000000000..4eac949ba --- /dev/null +++ b/modules/monitoring/library/Monitoring/BackendStep.php @@ -0,0 +1,175 @@ +data = $data; + } + + public function apply() + { + $success = $this->createBackendsIni(); + $success &= $this->createResourcesIni(); + return $success; + } + + protected function createBackendsIni() + { + $config = array(); + $config[$this->data['backendConfig']['name']] = array( + 'type' => $this->data['backendConfig']['type'], + 'resource' => $this->data['resourceConfig']['name'] + ); + + try { + $writer = new IniWriter(array( + 'config' => new Config($config), + 'filename' => Config::resolvePath('modules/monitoring/backends.ini') + )); + $writer->write(); + } catch (Exception $e) { + $this->backendIniError = $e; + return false; + } + + $this->backendIniError = false; + return true; + } + + protected function createResourcesIni() + { + $resourceConfig = $this->data['resourceConfig']; + $resourceName = $resourceConfig['name']; + unset($resourceConfig['name']); + + try { + $config = Config::app('resources', true); + $config->merge(new Config(array($resourceName => $resourceConfig))); + + $writer = new IniWriter(array( + 'config' => $config, + 'filename' => Config::resolvePath('resources.ini'), + 'filemode' => 0660 + )); + $writer->write(); + } catch (Exception $e) { + $this->resourcesIniError = $e; + return false; + } + + $this->resourcesIniError = false; + return true; + } + + public function getSummary() + { + $pageTitle = '

' . mt('monitoring', 'Monitoring Backend', 'setup.page.title') . '

'; + $backendDescription = '

' . sprintf( + mt( + 'monitoring', + 'Icinga Web 2 will retrieve information from your monitoring environment' + . ' using a backend called "%s" and the specified resource below:' + ), + $this->data['backendConfig']['name'] + ) . '

'; + + if ($this->data['resourceConfig']['type'] === 'db') { + $resourceTitle = '

' . mt('monitoring', 'Database Resource') . '

'; + $resourceHtml = '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
' . t('Resource Name') . '' . $this->data['resourceConfig']['name'] . '
' . t('Database Type') . '' . $this->data['resourceConfig']['db'] . '
' . t('Host') . '' . $this->data['resourceConfig']['host'] . '
' . t('Port') . '' . $this->data['resourceConfig']['port'] . '
' . t('Database Name') . '' . $this->data['resourceConfig']['dbname'] . '
' . t('Username') . '' . $this->data['resourceConfig']['username'] . '
' . t('Password') . '' . str_repeat('*', strlen($this->data['resourceConfig']['password'])) . '
'; + } else { // $this->data['resourceConfig']['type'] === 'livestatus' + $resourceTitle = '

' . mt('monitoring', 'Livestatus Resource') . '

'; + $resourceHtml = '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
' . t('Resource Name') . '' . $this->data['resourceConfig']['name'] . '
' . t('Socket') . '' . $this->data['resourceConfig']['socket'] . '
'; + } + + return $pageTitle . '
' . $backendDescription . $resourceTitle . $resourceHtml . '
'; + } + + public function getReport() + { + $report = ''; + if ($this->backendIniError === false) { + $message = mt('monitoring', 'Monitoring backend configuration has been successfully written to: %s'); + $report .= '

' . sprintf($message, Config::resolvePath('modules/monitoring/backends.ini')) . '

'; + } elseif ($this->backendIniError !== null) { + $message = mt( + 'monitoring', + 'Monitoring backend configuration could not be written to: %s; An error occured:' + ); + $report .= '

' . sprintf( + $message, + Config::resolvePath('modules/monitoring/backends.ini') + ) . '

' . $this->backendIniError->getMessage() . '

'; + } + + if ($this->resourcesIniError === false) { + $message = mt('monitoring', 'Resource configuration has been successfully updated: %s'); + $report .= '

' . sprintf($message, Config::resolvePath('resources.ini')) . '

'; + } elseif ($this->resourcesIniError !== null) { + $message = mt('monitoring', 'Resource configuration could not be udpated: %s; An error occured:'); + $report .= '

' . sprintf($message, Config::resolvePath('resources.ini')) . '

' + . '

' . $this->resourcesIniError->getMessage() . '

'; + } + + return $report; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php index e5c9f7ba1..166bbb6a3 100644 --- a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php +++ b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php @@ -4,7 +4,6 @@ namespace Icinga\Module\Monitoring\Command\Transport; -use Zend_Config; use Icinga\Application\Config; use Icinga\Exception\ConfigurationError; @@ -45,12 +44,12 @@ abstract class CommandTransport /** * Create a transport from config * - * @param Zend_Config $config + * @param Config $config * * @return LocalCommandFile|RemoteCommandFile * @throws ConfigurationError */ - public static function fromConfig(Zend_Config $config) + public static function fromConfig(Config $config) { switch (strtolower($config->transport)) { case RemoteCommandFile::TRANSPORT: diff --git a/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php b/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php index c3133d469..73a2e5288 100644 --- a/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php +++ b/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php @@ -6,7 +6,7 @@ namespace Icinga\Module\Monitoring\Command\Transport; use Exception; use LogicException; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Module\Monitoring\Command\Exception\TransportException; use Icinga\Module\Monitoring\Command\IcingaCommand; use Icinga\Module\Monitoring\Command\Renderer\IcingaCommandFileCommandRenderer; diff --git a/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php b/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php index d22e2c863..0e447a4f3 100644 --- a/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php +++ b/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php @@ -5,7 +5,7 @@ namespace Icinga\Module\Monitoring\Command\Transport; use LogicException; -use Icinga\Logger\Logger; +use Icinga\Application\Logger; use Icinga\Module\Monitoring\Command\Exception\TransportException; use Icinga\Module\Monitoring\Command\IcingaCommand; use Icinga\Module\Monitoring\Command\Renderer\IcingaCommandFileCommandRenderer; diff --git a/modules/monitoring/library/Monitoring/DataView/Customvar.php b/modules/monitoring/library/Monitoring/DataView/Customvar.php index 582064c16..0a8887fde 100644 --- a/modules/monitoring/library/Monitoring/DataView/Customvar.php +++ b/modules/monitoring/library/Monitoring/DataView/Customvar.php @@ -19,6 +19,7 @@ class Customvar extends DataView return array( 'varname', 'varvalue', + 'is_json', 'object_type' ); } diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php index 2c4ec5380..1fdd8b4f9 100644 --- a/modules/monitoring/library/Monitoring/DataView/DataView.php +++ b/modules/monitoring/library/Monitoring/DataView/DataView.php @@ -15,7 +15,7 @@ use Icinga\Data\Filterable; use Icinga\Exception\QueryException; use Icinga\Web\Request; use Icinga\Web\Url; -use Icinga\Module\Monitoring\Backend; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; /** * A read-only view of an underlying query @@ -44,8 +44,7 @@ abstract class DataView implements Browsable, Countable, Filterable, Sortable public function __construct(ConnectionInterface $connection, array $columns = null) { $this->connection = $connection; - $queryClass = $connection->getQueryClass($this->getQueryName()); - $this->query = new $queryClass($this->connection->getResource(), $columns); + $this->query = $connection->query($this->getQueryName(), $columns); $this->filter = Filter::matchAll(); $this->init(); } @@ -105,7 +104,7 @@ abstract class DataView implements Browsable, Countable, Filterable, Sortable */ public static function fromRequest($request, array $columns = null) { - $view = new static(Backend::createBackend($request->getParam('backend')), $columns); + $view = new static(MonitoringBackend::instance($request->getParam('backend')), $columns); $view->applyUrlFilter($request); return $view; @@ -141,7 +140,7 @@ abstract class DataView implements Browsable, Countable, Filterable, Sortable */ public static function fromParams(array $params, array $columns = null) { - $view = new static(Backend::createBackend($params['backend']), $columns); + $view = new static(MonitoringBackend::instance($params['backend']), $columns); foreach ($params as $key => $value) { if ($view->isValidFilterTarget($key)) { diff --git a/modules/monitoring/library/Monitoring/DataView/Downtime.php b/modules/monitoring/library/Monitoring/DataView/Downtime.php index 217fb2cac..e075d658c 100644 --- a/modules/monitoring/library/Monitoring/DataView/Downtime.php +++ b/modules/monitoring/library/Monitoring/DataView/Downtime.php @@ -16,6 +16,7 @@ class Downtime extends DataView return array( 'downtime_objecttype', 'downtime_author', + 'author', 'downtime_comment', 'downtime_entry_time', 'downtime_is_fixed', diff --git a/modules/monitoring/library/Monitoring/InstanceStep.php b/modules/monitoring/library/Monitoring/InstanceStep.php new file mode 100644 index 000000000..9a5ce4f1b --- /dev/null +++ b/modules/monitoring/library/Monitoring/InstanceStep.php @@ -0,0 +1,103 @@ +data = $data; + } + + public function apply() + { + $instanceConfig = $this->data['instanceConfig']; + $instanceName = $instanceConfig['name']; + unset($instanceConfig['name']); + + try { + $writer = new IniWriter(array( + 'config' => new Config(array($instanceName => $instanceConfig)), + 'filename' => Config::resolvePath('modules/monitoring/instances.ini') + )); + $writer->write(); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + $pageTitle = '

' . mt('monitoring', 'Monitoring Instance', 'setup.page.title') . '

'; + + if (isset($this->data['instanceConfig']['host'])) { + $pipeHtml = '

' . sprintf( + mt( + 'monitoring', + 'Icinga Web 2 will use the named pipe located on a remote machine at "%s" to send commands' + . ' to your monitoring instance by using the connection details listed below:' + ), + $this->data['instanceConfig']['path'] + ) . '

'; + + $pipeHtml .= '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
' . mt('monitoring', 'Remote Host') . '' . $this->data['instanceConfig']['host'] . '
' . mt('monitoring', 'Remote SSH Port') . '' . $this->data['instanceConfig']['port'] . '
' . mt('monitoring', 'Remote SSH User') . '' . $this->data['instanceConfig']['user'] . '
'; + } else { + $pipeHtml = '

' . sprintf( + mt( + 'monitoring', + 'Icinga Web 2 will use the named pipe located at "%s"' + . ' to send commands to your monitoring instance.' + ), + $this->data['instanceConfig']['path'] + ) . '

'; + } + + return $pageTitle . '
' . $pipeHtml . '
'; + } + + public function getReport() + { + if ($this->error === false) { + $message = mt('monitoring', 'Monitoring instance configuration has been successfully created: %s'); + return '

' . sprintf($message, Config::resolvePath('modules/monitoring/instances.ini')) . '

'; + } elseif ($this->error !== null) { + $message = mt( + 'monitoring', + 'Monitoring instance configuration could not be written to: %s; An error occured:' + ); + return '

' . sprintf($message, Config::resolvePath('modules/monitoring/instances.ini')) + . '

' . $this->error->getMessage() . '

'; + } + } +} diff --git a/modules/monitoring/library/Monitoring/MonitoringWizard.php b/modules/monitoring/library/Monitoring/MonitoringWizard.php new file mode 100644 index 000000000..6ccd4b22b --- /dev/null +++ b/modules/monitoring/library/Monitoring/MonitoringWizard.php @@ -0,0 +1,179 @@ +addPage(new WelcomePage()); + $this->addPage(new BackendPage()); + $this->addPage(new IdoResourcePage()); + $this->addPage(new LivestatusResourcePage()); + $this->addPage(new InstancePage()); + $this->addPage(new SecurityPage()); + $this->addPage(new SummaryPage()); + } + + /** + * @see Wizard::setupPage() + */ + public function setupPage(Form $page, Request $request) + { + if ($page->getName() === 'setup_requirements') { + $page->setRequirements($this->getRequirements()); + } elseif ($page->getName() === 'setup_summary') { + $page->setSummary($this->getSetup()->getSummary()); + $page->setSubjectTitle(mt('monitoring', 'the monitoring module', 'setup.summary.subject')); + } elseif ( + $this->getDirection() === static::FORWARD + && ($page->getName() === 'setup_monitoring_ido' || $page->getName() === 'setup_monitoring_livestatus') + ) { + if ((($dbResourceData = $this->getPageData('setup_db_resource')) !== null + && $dbResourceData['name'] === $request->getPost('name')) + || (($ldapResourceData = $this->getPageData('setup_ldap_resource')) !== null + && $ldapResourceData['name'] === $request->getPost('name')) + ) { + $page->addError(mt('monitoring', 'The given resource name is already in use.')); + } + } + } + + /** + * @see Wizard::getNewPage() + */ + protected function getNewPage($requestedPage, Form $originPage) + { + $skip = false; + $newPage = parent::getNewPage($requestedPage, $originPage); + if ($newPage->getName() === 'setup_monitoring_ido') { + $backendData = $this->getPageData('setup_monitoring_backend'); + $skip = $backendData['type'] !== 'ido'; + } elseif ($newPage->getName() === 'setup_monitoring_livestatus') { + $backendData = $this->getPageData('setup_monitoring_backend'); + $skip = $backendData['type'] !== 'livestatus'; + } + + if ($skip) { + if ($this->hasPageData($newPage->getName())) { + $pageData = & $this->getPageData(); + unset($pageData[$newPage->getName()]); + } + + $pages = $this->getPages(); + if ($this->getDirection() === static::FORWARD) { + $nextPage = $pages[array_search($newPage, $pages, true) + 1]; + $newPage = $this->getNewPage($nextPage->getName(), $newPage); + } else { // $this->getDirection() === static::BACKWARD + $previousPage = $pages[array_search($newPage, $pages, true) - 1]; + $newPage = $this->getNewPage($previousPage->getName(), $newPage); + } + } + + return $newPage; + } + + /** + * @see Wizard::addButtons() + */ + protected function addButtons(Form $page) + { + parent::addButtons($page); + + $pages = $this->getPages(); + $index = array_search($page, $pages, true); + if ($index === 0) { + // Used t() here as "Start" is too generic and already translated in the icinga domain + $page->getElement(static::BTN_NEXT)->setLabel(t('Start', 'setup.welcome.btn.next')); + } elseif ($index === count($pages) - 1) { + $page->getElement(static::BTN_NEXT)->setLabel( + mt('monitoring', 'Setup the monitoring module for Icinga Web 2', 'setup.summary.btn.finish') + ); + } + } + + /** + * @see SetupWizard::getSetup() + */ + public function getSetup() + { + $pageData = $this->getPageData(); + $setup = new Setup(); + + $setup->addStep(new MakeDirStep(array($this->getConfigDir() . '/modules/monitoring'), 0775)); + + $setup->addStep( + new BackendStep(array( + 'backendConfig' => $pageData['setup_monitoring_backend'], + 'resourceConfig' => isset($pageData['setup_monitoring_ido']) + ? array_diff_key($pageData['setup_monitoring_ido'], array('skip_validation' => null)) + : array_diff_key($pageData['setup_monitoring_livestatus'], array('skip_validation' => null)) + )) + ); + + $setup->addStep( + new InstanceStep(array( + 'instanceConfig' => $pageData['setup_monitoring_instance'] + )) + ); + + $setup->addStep( + new SecurityStep(array( + 'securityConfig' => $pageData['setup_monitoring_security'] + )) + ); + + $setup->addStep(new EnableModuleStep('monitoring')); + + return $setup; + } + + /** + * @see SetupWizard::getRequirements() + */ + public function getRequirements() + { + return new Requirements(); + } + + /** + * Return the configuration directory of Icinga Web 2 + * + * @return string + */ + protected function getConfigDir() + { + if (array_key_exists('ICINGAWEB_CONFIGDIR', $_SERVER)) { + $configDir = $_SERVER['ICINGAWEB_CONFIGDIR']; + } else { + $configDir = '/etc/icingaweb'; + } + + $canonical = realpath($configDir); + return $canonical ? $canonical : $configDir; + } +} diff --git a/modules/monitoring/library/Monitoring/Object/Host.php b/modules/monitoring/library/Monitoring/Object/Host.php index 455d29980..831579a57 100644 --- a/modules/monitoring/library/Monitoring/Object/Host.php +++ b/modules/monitoring/library/Monitoring/Object/Host.php @@ -5,7 +5,7 @@ namespace Icinga\Module\Monitoring\Object; use InvalidArgumentException; -use Icinga\Module\Monitoring\Backend; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; /** * A Icinga host @@ -63,10 +63,10 @@ class Host extends MonitoredObject /** * Create a new host * - * @param Backend $backend Backend to fetch host information from + * @param MonitoringBackend $backend Backend to fetch host information from * @param string $host Host name */ - public function __construct(Backend $backend, $host) + public function __construct(MonitoringBackend $backend, $host) { parent::__construct($backend); $this->host = $host; @@ -130,7 +130,8 @@ class Host extends MonitoredObject 'host_notes_url', 'host_modified_host_attributes', 'host_problem', - 'host_process_performance_data' + 'host_process_performance_data', + 'process_perfdata' => 'host_process_performance_data' )) ->where('host_name', $this->host); } diff --git a/modules/monitoring/library/Monitoring/Object/MonitoredObject.php b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php index 3d3bde8dd..8e88b861b 100644 --- a/modules/monitoring/library/Monitoring/Object/MonitoredObject.php +++ b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php @@ -7,7 +7,7 @@ namespace Icinga\Module\Monitoring\Object; use InvalidArgumentException; use Icinga\Application\Config; use Icinga\Exception\InvalidPropertyException; -use Icinga\Module\Monitoring\Backend; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; use Icinga\Web\UrlParams; /** @@ -28,7 +28,7 @@ abstract class MonitoredObject /** * Backend to fetch object information from * - * @var Backend + * @var MonitoringBackend */ protected $backend; @@ -119,9 +119,9 @@ abstract class MonitoredObject /** * Create a monitored object, i.e. host or service * - * @param Backend $backend Backend to fetch object information from + * @param MonitoringBackend $backend Backend to fetch object information from */ - public function __construct(Backend $backend) + public function __construct(MonitoringBackend $backend) { $this->backend = $backend; } @@ -283,7 +283,8 @@ abstract class MonitoredObject $query = $this->backend->select()->from('customvar', array( 'varname', - 'varvalue' + 'varvalue', + 'is_json' )) ->where('object_type', $this->type) ->where('host_name', $this->host_name); @@ -293,13 +294,16 @@ abstract class MonitoredObject $this->customvars = array(); - $customvars = $query->getQuery()->fetchPairs(); - foreach ($customvars as $name => $value) { - $name = ucwords(str_replace('_', ' ', strtolower($name))); - if ($blacklistPattern && preg_match($blacklistPattern, $name)) { - $value = '***'; + $customvars = $query->getQuery()->fetchAll(); + foreach ($customvars as $name => $cv) { + $name = ucwords(str_replace('_', ' ', strtolower($cv->varname))); + if ($blacklistPattern && preg_match($blacklistPattern, $cv->varname)) { + $this->customvars[$name] = '***'; + } elseif ($cv->is_json) { + $this->customvars[$name] = json_decode($cv->varvalue); + } else { + $this->customvars[$name] = $cv->varvalue; } - $this->customvars[$name] = $value; } return $this; @@ -317,10 +321,12 @@ abstract class MonitoredObject 'contact_alias', 'contact_email', 'contact_pager', - )) - ->where('host_name', $this->host_name); + )); if ($this->type === self::TYPE_SERVICE) { + $contacts->where('service_host_name', $this->host_name); $contacts->where('service_description', $this->service_description); + } else { + $contacts->where('host_name', $this->host_name); } $this->contacts = $contacts->getQuery()->fetchAll(); return $this; @@ -474,9 +480,9 @@ abstract class MonitoredObject public static function fromParams(UrlParams $params) { if ($params->has('service') && $params->has('host')) { - return new Service(Backend::createBackend(), $params->get('host'), $params->get('service')); + return new Service(MonitoringBackend::instance(), $params->get('host'), $params->get('service')); } elseif ($params->has('host')) { - return new Host(Backend::createBackend(), $params->get('host')); + return new Host(MonitoringBackend::instance(), $params->get('host')); } return null; } diff --git a/modules/monitoring/library/Monitoring/Object/ObjectList.php b/modules/monitoring/library/Monitoring/Object/ObjectList.php index 1e5595d58..6eb1e4db3 100644 --- a/modules/monitoring/library/Monitoring/Object/ObjectList.php +++ b/modules/monitoring/library/Monitoring/Object/ObjectList.php @@ -5,7 +5,7 @@ namespace Icinga\Module\Monitoring\Object; use ArrayIterator; use Countable; use IteratorAggregate; -use Icinga\Module\Monitoring\Backend; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; abstract class ObjectList implements Countable, IteratorAggregate { @@ -21,7 +21,7 @@ abstract class ObjectList implements Countable, IteratorAggregate protected $count; - public function __construct(Backend $backend) + public function __construct(MonitoringBackend $backend) { $this->backend = $backend; } diff --git a/modules/monitoring/library/Monitoring/Object/Service.php b/modules/monitoring/library/Monitoring/Object/Service.php index 7a0812d8b..6d5245f0d 100644 --- a/modules/monitoring/library/Monitoring/Object/Service.php +++ b/modules/monitoring/library/Monitoring/Object/Service.php @@ -5,7 +5,7 @@ namespace Icinga\Module\Monitoring\Object; use InvalidArgumentException; -use Icinga\Module\Monitoring\Backend; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; /** * A Icinga service @@ -68,11 +68,11 @@ class Service extends MonitoredObject /** * Create a new service * - * @param Backend $backend Backend to fetch service information from + * @param MonitoringBackend $backend Backend to fetch service information from * @param string $host Host name the service is running on * @param string $service Service name */ - public function __construct(Backend $backend, $host, $service) + public function __construct(MonitoringBackend $backend, $host, $service) { parent::__construct($backend); $this->host = new Host($backend, $host); @@ -191,6 +191,7 @@ class Service extends MonitoredObject 'service_flap_detection_enabled_changed', 'service_modified_service_attributes', 'service_process_performance_data', + 'process_perfdata' => 'service_process_performance_data', 'service_percent_state_change', 'service_host_name' )) diff --git a/modules/monitoring/library/Monitoring/SecurityStep.php b/modules/monitoring/library/Monitoring/SecurityStep.php new file mode 100644 index 000000000..636f5826e --- /dev/null +++ b/modules/monitoring/library/Monitoring/SecurityStep.php @@ -0,0 +1,81 @@ +data = $data; + } + + public function apply() + { + $config = array(); + $config['security'] = $this->data['securityConfig']; + + try { + $writer = new IniWriter(array( + 'config' => new Config($config), + 'filename' => Config::resolvePath('modules/monitoring/config.ini') + )); + $writer->write(); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + $pageTitle = '

' . mt('monitoring', 'Monitoring Security', 'setup.page.title') . '

'; + $pageDescription = '

' . mt( + 'monitoring', + 'Icinga Web 2 will protect your monitoring environment against' + . ' prying eyes using the configuration specified below:' + ) . '

'; + + $pageHtml = '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
' . mt('monitoring', 'Protected Custom Variables') . '' . ($this->data['securityConfig']['protected_customvars'] ? ( + $this->data['securityConfig']['protected_customvars'] + ) : mt('monitoring', 'None', 'monitoring.protected_customvars')) . '
'; + + return $pageTitle . '
' . $pageDescription . $pageHtml . '
'; + } + + public function getReport() + { + if ($this->error === false) { + $message = mt('monitoring', 'Monitoring security configuration has been successfully created: %s'); + return '

' . sprintf($message, Config::resolvePath('modules/monitoring/config.ini')) . '

'; + } elseif ($this->error !== null) { + $message = mt( + 'monitoring', + 'Monitoring security configuration could not be written to: %s; An error occured:' + ); + return '

' . sprintf($message, Config::resolvePath('modules/monitoring/config.ini')) + . '

' . $this->error->getMessage() . '

'; + } + } +} \ No newline at end of file diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeLine.php b/modules/monitoring/library/Monitoring/Timeline/TimeLine.php index 3a795df32..93dc5a0ef 100644 --- a/modules/monitoring/library/Monitoring/Timeline/TimeLine.php +++ b/modules/monitoring/library/Monitoring/Timeline/TimeLine.php @@ -288,7 +288,6 @@ class TimeLine implements IteratorAggregate if ($this->session !== null) { $this->session->calculationBase = $new; - $this->session->write(); } } else { $this->calculationBase = $calculationBase; diff --git a/modules/monitoring/public/css/module.less b/modules/monitoring/public/css/module.less index 7e734bb33..6e0124ccd 100644 --- a/modules/monitoring/public/css/module.less +++ b/modules/monitoring/public/css/module.less @@ -135,26 +135,9 @@ form.instance-features span.description, form.object-features span.description { display: inline; } -.alertsummary-flex-container { - display: -ms-Flexbox; - -ms-box-orient: horizontal; - - display: -webkit-flex; - display: -moz-flex; - display: -ms-flex; - display: flex; - - -webkit-flex-flow: row wrap; - -moz-flex-flow: row wrap; - -ms-flex-flow: row wrap; - flex-flow: row wrap; -} - -.alertsummary-flex { - flex: 1 1 auto; - overflow: auto; - border: 1px #333 solid; - padding: 5px; - margin: 0 10px 0 0; - border-spacing: 10px 10px; +table.avp .customvar ul { + list-style-type: none; + margin: 0; + padding: 0; + padding-left: 1.5em; } diff --git a/modules/monitoring/test/php/regression/Bug7043Test.php b/modules/monitoring/test/php/regression/Bug7043Test.php index 37618a348..ea216d6fa 100644 --- a/modules/monitoring/test/php/regression/Bug7043Test.php +++ b/modules/monitoring/test/php/regression/Bug7043Test.php @@ -10,7 +10,16 @@ use Icinga\Application\Config; use Icinga\Module\Monitoring\Backend; use Icinga\Test\BaseTestCase; use Mockery; -use Zend_Config; + + +class ConfigWithSetModuleConfig extends Config +{ + public static function setModuleConfig($moduleName, $configName, $config) + { + static::$modules[$moduleName][$configName] = $config; + } +} + class Bug7043Test extends BaseTestCase { @@ -36,7 +45,7 @@ class Bug7043Test extends BaseTestCase ->getMock() ); - Config::setModuleConfig('monitoring', 'backends', new Zend_Config(array( + ConfigWithSetModuleConfig::setModuleConfig('monitoring', 'backends', new Config(array( 'backendName' => array( 'type' => 'ido', 'resource' => 'ido' diff --git a/modules/setup/application/clicommands/ConfigCommand.php b/modules/setup/application/clicommands/ConfigCommand.php new file mode 100644 index 000000000..0006f6ce3 --- /dev/null +++ b/modules/setup/application/clicommands/ConfigCommand.php @@ -0,0 +1,63 @@ + [options] + * + * OPTIONS: + * + * --mode The access mode to use. Default is: 2770 + * --path The path to the configuration directory. If omitted the default is used. + * + * EXAMPLES: + * + * icingacli setup config createDirectory apache + * icingacli setup config createDirectory apache --mode 2775 + * icingacli setup config createDirectory apache --path /some/path + */ + public function createDirectoryAction() + { + $group = $this->params->getStandalone(); + if ($group === null) { + $this->fail($this->translate('The `group\' argument is mandatory.')); + return false; + } + + $path = $this->params->get('path', $this->app->getConfigDir()); + if (file_exists($path)) { + printf($this->translate("Configuration directory already exists at: %s\n"), $path); + return true; + } + + $mode = octdec($this->params->get('mode', '2770')); + if (false === mkdir($path)) { + $this->fail(sprintf($this->translate('Unable to create path: %s'), $path)); + return false; + } + + $old = umask(0); // Prevent $mode from being mangled by the system's umask ($old) + chmod($path, $mode); + umask($old); + + if (chgrp($path, $group) === false) { + $this->fail(sprintf($this->translate('Unable to change the group of "%s" to "%s".'), $path, $group)); + return false; + } + + printf($this->translate("Successfully created configuration directory at: %s\n"), $path); + } +} diff --git a/modules/setup/application/clicommands/TokenCommand.php b/modules/setup/application/clicommands/TokenCommand.php new file mode 100644 index 000000000..ff0229cfe --- /dev/null +++ b/modules/setup/application/clicommands/TokenCommand.php @@ -0,0 +1,68 @@ + + */ +class TokenCommand extends Command +{ + /** + * Display the current setup token + * + * Shows you the current setup token used to authenticate when setting up Icinga Web 2 using the web-based wizard. + * + * USAGE: + * + * icingacli setup token show + */ + public function showAction() + { + $token = file_get_contents($this->app->getConfigDir() . '/setup.token'); + if (! $token) { + $this->fail( + $this->translate('Nothing to show. Please create a new setup token using the generateToken action.') + ); + } + + printf($this->translate("The current setup token is: %s\n"), $token); + } + + /** + * Create a new setup token + * + * Re-generates the setup token used to authenticate when setting up Icinga Web 2 using the web-based wizard. + * + * USAGE: + * + * icingacli setup token create + */ + public function createAction() + { + if (function_exists('openssl_random_pseudo_bytes')) { + $token = bin2hex(openssl_random_pseudo_bytes(8)); + } else { + $token = substr(md5(mt_rand()), 16); + } + + $filepath = $this->app->getConfigDir() . '/setup.token'; + + if (false === file_put_contents($filepath, $token)) { + $this->fail(sprintf($this->translate('Cannot write setup token "%s" to disk.'), $filepath)); + } + + if (false === chmod($filepath, 0660)) { + $this->fail(sprintf($this->translate('Cannot change access mode of "%s" to %o.'), $filepath, 0660)); + } + + printf($this->translate("The newly generated setup token is: %s\n"), $token); + } +} diff --git a/modules/setup/application/controllers/IndexController.php b/modules/setup/application/controllers/IndexController.php new file mode 100644 index 000000000..113886c88 --- /dev/null +++ b/modules/setup/application/controllers/IndexController.php @@ -0,0 +1,43 @@ +isFinished()) { + $setup = $wizard->getSetup(); + $success = $setup->run(); + if ($success) { + $wizard->clearSession(); + } else { + $wizard->setIsFinished(false); + } + + $this->view->success = $success; + $this->view->report = $setup->getReport(); + } else { + $wizard->handleRequest(); + } + + $this->view->wizard = $wizard; + } +} diff --git a/modules/setup/application/forms/AdminAccountPage.php b/modules/setup/application/forms/AdminAccountPage.php new file mode 100644 index 000000000..38101fe85 --- /dev/null +++ b/modules/setup/application/forms/AdminAccountPage.php @@ -0,0 +1,283 @@ +setName('setup_admin_account'); + $this->setViewScript('form/setup-admin-account.phtml'); + } + + /** + * Set the resource configuration to use + * + * @param array $config + * + * @return self + */ + public function setResourceConfig(array $config) + { + $this->resourceConfig = $config; + return $this; + } + + /** + * Set the backend configuration to use + * + * @param array $config + * + * @return self + */ + public function setBackendConfig(array $config) + { + $this->backendConfig = $config; + return $this; + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $choices = array(); + + if ($this->backendConfig['backend'] !== 'db') { + $choices['by_name'] = mt('setup', 'By Name', 'setup.admin'); + $this->addElement( + 'text', + 'by_name', + array( + 'required' => isset($formData['user_type']) && $formData['user_type'] === 'by_name', + 'value' => $this->getUsername(), + 'label' => mt('setup', 'Username'), + 'description' => mt( + 'setup', + 'Define the initial administrative account by providing a username that reflects' + . ' a user created later or one that is authenticated using external mechanisms' + ) + ) + ); + } + + if ($this->backendConfig['backend'] === 'db' || $this->backendConfig['backend'] === 'ldap') { + $users = $this->fetchUsers(); + if (false === empty($users)) { + $choices['existing_user'] = mt('setup', 'Existing User'); + $this->addElement( + 'select', + 'existing_user', + array( + 'required' => isset($formData['user_type']) && $formData['user_type'] === 'existing_user', + 'label' => mt('setup', 'Username'), + 'description' => sprintf( + mt( + 'setup', + 'Choose a user reported by the %s backend as the initial administrative account', + 'setup.admin' + ), + $this->backendConfig['backend'] === 'db' + ? mt('setup', 'database', 'setup.admin.authbackend') + : 'LDAP' + ), + 'multiOptions' => array_combine($users, $users) + ) + ); + } + } + + if ($this->backendConfig['backend'] === 'db') { + $choices['new_user'] = mt('setup', 'New User'); + $required = isset($formData['user_type']) && $formData['user_type'] === 'new_user'; + $this->addElement( + 'text', + 'new_user', + array( + 'required' => $required, + 'label' => mt('setup', 'Username'), + 'description' => mt( + 'setup', + 'Enter the username to be used when creating an initial administrative account' + ) + ) + ); + $this->addElement( + 'password', + 'new_user_password', + array( + 'required' => $required, + 'label' => mt('setup', 'Password'), + 'description' => mt('setup', 'Enter the password to assign to the newly created account') + ) + ); + $this->addElement( + 'password', + 'new_user_2ndpass', + array( + 'required' => $required, + 'label' => mt('setup', 'Repeat password'), + 'description' => mt('setup', 'Please repeat the password given above to avoid typing errors'), + 'validators' => array( + array('identical', false, array('new_user_password')) + ) + ) + ); + } + + if (count($choices) > 1) { + $this->addElement( + 'radio', + 'user_type', + array( + 'required' => true, + 'multiOptions' => $choices + ) + ); + } else { + $this->addElement( + 'hidden', + 'user_type', + array( + 'required' => true, + 'value' => key($choices) + ) + ); + } + + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('setup', 'Administration', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => tp( + 'Now it\'s time to configure your first administrative account for Icinga Web 2.' + . ' Please follow the instructions below:', + 'Now it\'s time to configure your first administrative account for Icinga Web 2.' + . ' Below are several options you can choose from. Select one and follow its instructions:', + count($choices) + ) + ) + ) + ); + } + + /** + * Validate the given request data and ensure that any new user does not already exist + * + * @param array $data The request data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if ($data['user_type'] === 'new_user' && array_search($data['new_user'], $this->fetchUsers()) !== false) { + $this->getElement('new_user')->addError(mt('setup', 'Username already exists.')); + return false; + } + + return true; + } + + /** + * Return the name of the externally authenticated user + * + * @return string + */ + protected function getUsername() + { + if (false === isset($_SERVER['REMOTE_USER'])) { + return ''; + } + + $name = $_SERVER['REMOTE_USER']; + if (isset($this->backendConfig['strip_username_regexp']) && $this->backendConfig['strip_username_regexp']) { + // No need to silence or log anything here because the pattern has + // already been successfully compiled during backend configuration + $name = preg_replace($this->backendConfig['strip_username_regexp'], '', $name); + } + + return $name; + } + + /** + * Return the names of all users this backend currently provides + * + * @return array + * + * @throws LogicException In case the backend to fetch users from is not supported + */ + protected function fetchUsers() + { + if ($this->backendConfig['backend'] === 'db') { + $backend = new DbUserBackend(ResourceFactory::createResource(new Config($this->resourceConfig))); + } elseif ($this->backendConfig['backend'] === 'ldap') { + $backend = new LdapUserBackend( + ResourceFactory::createResource(new Config($this->resourceConfig)), + $this->backendConfig['user_class'], + $this->backendConfig['user_name_attribute'], + $this->backendConfig['base_dn'] + ); + } else { + throw new LogicException( + sprintf( + 'Tried to fetch users from an unsupported authentication backend: %s', + $this->backendConfig['backend'] + ) + ); + } + + try { + return $backend->listUsers(); + } catch (Exception $e) { + // No need to handle anything special here. Error means no users found. + return array(); + } + } +} diff --git a/modules/setup/application/forms/AuthBackendPage.php b/modules/setup/application/forms/AuthBackendPage.php new file mode 100644 index 000000000..89a060a06 --- /dev/null +++ b/modules/setup/application/forms/AuthBackendPage.php @@ -0,0 +1,161 @@ +setName('setup_authentication_backend'); + } + + /** + * Set the resource configuration to use + * + * @param array $config + * + * @return self + */ + public function setResourceConfig(array $config) + { + $this->config = $config; + return $this; + } + + /** + * Return the resource configuration as Config object + * + * @return Config + */ + public function getResourceConfig() + { + return new Config($this->config); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('setup', 'Authentication Backend', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + + if ($this->config['type'] === 'db') { + $note = mt( + 'setup', + 'As you\'ve chosen to use a database for authentication all you need ' + . 'to do now is defining a name for your first authentication backend.' + ); + } elseif ($this->config['type'] === 'ldap') { + $note = mt( + 'setup', + 'Before you are able to authenticate using the LDAP connection defined earlier you need to' + . ' provide some more information so that Icinga Web 2 is able to locate account details.' + ); + } else { // if ($this->config['type'] === 'autologin' + $note = mt( + 'setup', + 'You\'ve chosen to authenticate using a web server\'s mechanism so it may be necessary' + . ' to adjust usernames before any permissions, restrictions, etc. are being applied.' + ); + } + + $this->addElement( + new Note( + 'description', + array('value' => $note) + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + $this->addSkipValidationCheckbox(); + } + + if ($this->config['type'] === 'db') { + $backendForm = new DbBackendForm(); + $backendForm->createElements($formData)->removeElement('resource'); + } elseif ($this->config['type'] === 'ldap') { + $backendForm = new LdapBackendForm(); + $backendForm->createElements($formData)->removeElement('resource'); + } else { // $this->config['type'] === 'autologin' + $backendForm = new AutologinBackendForm(); + $backendForm->createElements($formData); + } + + $this->addElements($backendForm->getElements()); + $this->getElement('name')->setValue('icingaweb'); + } + + /** + * Validate the given form data and check whether it's possible to authenticate using the configured backend + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) { + if ($this->config['type'] === 'ldap' && false === LdapBackendForm::isValidAuthenticationBackend($this)) { + $this->addSkipValidationCheckbox(); + return false; + } + } + + return true; + } + + /** + * Add a checkbox to this form by which the user can skip the authentication validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'order' => 2, + 'ignore' => true, + 'required' => true, + 'label' => mt('setup', 'Skip Validation'), + 'description' => mt('setup', 'Check this to not to validate authentication using this backend') + ) + ); + } +} diff --git a/modules/setup/application/forms/AuthenticationPage.php b/modules/setup/application/forms/AuthenticationPage.php new file mode 100644 index 000000000..345625c94 --- /dev/null +++ b/modules/setup/application/forms/AuthenticationPage.php @@ -0,0 +1,74 @@ +setName('setup_authentication_type'); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('setup', 'Authentication', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'setup', + 'Please choose how you want to authenticate when accessing Icinga Web 2.' + . ' Configuring backend specific details follows in a later step.' + ) + ) + ) + ); + + $backendTypes = array(); + if (Platform::extensionLoaded('mysql') || Platform::extensionLoaded('pgsql')) { + $backendTypes['db'] = t('Database'); + } + if (Platform::extensionLoaded('ldap')) { + $backendTypes['ldap'] = 'LDAP'; + } + $backendTypes['autologin'] = t('Autologin'); + + $this->addElement( + 'select', + 'type', + array( + 'required' => true, + 'label' => mt('setup', 'Authentication Type'), + 'description' => mt('setup', 'The type of authentication to use when accessing Icinga Web 2'), + 'multiOptions' => $backendTypes + ) + ); + } +} diff --git a/modules/setup/application/forms/DatabaseCreationPage.php b/modules/setup/application/forms/DatabaseCreationPage.php new file mode 100644 index 000000000..556e20e28 --- /dev/null +++ b/modules/setup/application/forms/DatabaseCreationPage.php @@ -0,0 +1,229 @@ +setName('setup_database_creation'); + } + + /** + * Set the resource configuration to use + * + * @param array $config + * + * @return self + */ + public function setResourceConfig(array $config) + { + $this->config = $config; + return $this; + } + + /** + * Set the required privileges to setup the database + * + * @param array $privileges The privileges + * + * @return self + */ + public function setDatabaseSetupPrivileges(array $privileges) + { + $this->databaseSetupPrivileges = $privileges; + return $this; + } + + /** + * Set the required privileges to operate the database + * + * @param array $privileges The privileges + * + * @return self + */ + public function setDatabaseUsagePrivileges(array $privileges) + { + $this->databaseUsagePrivileges = $privileges; + return $this; + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('setup', 'Database Setup', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'setup', + 'It seems that either the database you defined earlier does not yet exist and cannot be created' + . ' using the provided access credentials or the database does not have the required schema to ' + . 'be operated by Icinga Web 2. Please provide appropriate access credentials to solve this.' + ) + ) + ) + ); + + $skipValidation = isset($formData['skip_validation']) && $formData['skip_validation']; + $this->addElement( + 'text', + 'username', + array( + 'required' => false === $skipValidation, + 'label' => mt('setup', 'Username'), + 'description' => mt('setup', 'A user which is able to create databases and/or touch the database schema') + ) + ); + $this->addElement( + 'password', + 'password', + array( + 'label' => mt('setup', 'Password'), + 'description' => mt('setup', 'The password for the database user defined above') + ) + ); + + if ($skipValidation) { + $this->addSkipValidationCheckbox(); + } else { + $this->addElement( + 'hidden', + 'skip_validation', + array( + 'required' => true, + 'value' => 0 + ) + ); + } + } + + /** + * Validate the given form data and check whether the defined user has sufficient access rights + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if (isset($data['skip_validation']) && $data['skip_validation']) { + return true; + } + + $config = $this->config; + $config['username'] = $this->getValue('username'); + $config['password'] = $this->getValue('password'); + $db = new DbTool($config); + + try { + $db->connectToDb(); // Are we able to login on the database? + } catch (PDOException $_) { + try { + $db->connectToHost(); // Are we able to login on the server? + } catch (PDOException $e) { + // We are NOT able to login on the server.. + $this->addError($e->getMessage()); + $this->addSkipValidationCheckbox(); + return false; + } + } + + // In case we are connected the credentials filled into this + // form need to be granted to create databases, users... + if (false === $db->checkPrivileges($this->databaseSetupPrivileges)) { + $this->addError( + mt('setup', 'The provided credentials cannot be used to create the database and/or the user.') + ); + $this->addSkipValidationCheckbox(); + return false; + } + + // ...and to grant all required usage privileges to others + if (false === $db->isGrantable($this->databaseUsagePrivileges)) { + $this->addError(sprintf( + mt( + 'setup', + 'The provided credentials cannot be used to grant all required privileges to the login "%s".' + ), + $this->config['username'] + )); + $this->addSkipValidationCheckbox(); + return false; + } + + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the login and privilege validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'order' => 2, + 'required' => true, + 'label' => mt('setup', 'Skip Validation'), + 'description' => mt( + 'setup', + 'Check this to not to validate the ability to login and required privileges' + ) + ) + ); + } +} diff --git a/modules/setup/application/forms/DbResourcePage.php b/modules/setup/application/forms/DbResourcePage.php new file mode 100644 index 000000000..326f9131d --- /dev/null +++ b/modules/setup/application/forms/DbResourcePage.php @@ -0,0 +1,132 @@ +setName('setup_db_resource'); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + 'hidden', + 'type', + array( + 'required' => true, + 'value' => 'db' + ) + ); + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('setup', 'Database Resource', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'setup', + 'Now please configure your database resource. 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.' + ) + ) + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + $this->addSkipValidationCheckbox(); + } else { + $this->addElement( + 'hidden', + 'skip_validation', + array( + 'required' => true, + 'value' => 0 + ) + ); + } + + $resourceForm = new DbResourceForm(); + $this->addElements($resourceForm->createElements($formData)->getElements()); + $this->getElement('name')->setValue('icingaweb_db'); + $this->addElement( + 'hidden', + 'prefix', + array( + 'required' => true, + 'value' => 'icingaweb_' + ) + ); + } + + /** + * Validate the given form data and check whether it's possible to connect to the database server + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) { + try { + $db = new DbTool($this->getValues()); + $db->checkConnectivity(); + } catch (PDOException $e) { + $this->addError($e->getMessage()); + $this->addSkipValidationCheckbox(); + return false; + } + } + + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the connection validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'required' => true, + 'label' => mt('setup', 'Skip Validation'), + 'description' => mt('setup', 'Check this to not to validate connectivity with the given database server') + ) + ); + } +} diff --git a/modules/setup/application/forms/GeneralConfigPage.php b/modules/setup/application/forms/GeneralConfigPage.php new file mode 100644 index 000000000..36f138b43 --- /dev/null +++ b/modules/setup/application/forms/GeneralConfigPage.php @@ -0,0 +1,56 @@ +setName('setup_general_config'); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('setup', 'Application Configuration', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'setup', + 'Now please adjust all application and logging related configuration options to fit your needs.' + ) + ) + ) + ); + + $loggingForm = new LoggingConfigForm(); + $this->addElements($loggingForm->createElements($formData)->getElements()); + } +} diff --git a/modules/setup/application/forms/LdapDiscoveryConfirmPage.php b/modules/setup/application/forms/LdapDiscoveryConfirmPage.php new file mode 100644 index 000000000..bd312ca76 --- /dev/null +++ b/modules/setup/application/forms/LdapDiscoveryConfirmPage.php @@ -0,0 +1,154 @@ + + Type:{type} + Port:{port} + Root DN:{root_dn} + User Object Class:{user_class} + User Name Attribute:{user_attribute} + +EOT; + + /** + * The previous configuration + * + * @var array + */ + private $config; + + /** + * Initialize this page + */ + public function init() + { + $this->setName('setup_ldap_discovery_confirm'); + } + + /** + * Set the resource configuration to use + * + * @param array $config + * + * @return self + */ + public function setResourceConfig(array $config) + { + $this->config = $config; + return $this; + } + + /** + * Return the resource configuration as Config object + * + * @return Config + */ + public function getResourceConfig() + { + return new Config($this->config); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $resource = $this->config['resource']; + $backend = $this->config['backend']; + $html = $this->infoTemplate; + $html = str_replace('{type}', $this->config['type'], $html); + $html = str_replace('{hostname}', $resource['hostname'], $html); + $html = str_replace('{port}', $resource['port'], $html); + $html = str_replace('{root_dn}', $resource['root_dn'], $html); + $html = str_replace('{user_attribute}', $backend['user_name_attribute'], $html); + $html = str_replace('{user_class}', $backend['user_class'], $html); + + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('setup', 'LDAP Discovery Results', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => sprintf( + mt('setup', 'The following directory service has been found on domain "%s":'), + $this->config['domain'] + ) + ) + ) + ); + + $this->addElement( + new Note( + 'suggestion', + array( + 'value' => $html, + 'decorators' => array( + 'ViewHelper', + array( + 'HtmlTag', array('tag' => 'div') + ) + ) + ) + ) + ); + + $this->addElement( + 'checkbox', + 'confirm', + array( + 'value' => '1', + 'label' => mt('setup', 'Use this configuration?') + ) + ); + } + + /** + * Validate the given form data and check whether a BIND-request is successful + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + return true; + } + + public function getValues($suppressArrayNotation = false) + { + if ($this->getValue('confirm') === '1') { + // use configuration + return $this->config; + } + return null; + } +} diff --git a/modules/setup/application/forms/LdapDiscoveryPage.php b/modules/setup/application/forms/LdapDiscoveryPage.php new file mode 100644 index 000000000..7fe799d19 --- /dev/null +++ b/modules/setup/application/forms/LdapDiscoveryPage.php @@ -0,0 +1,109 @@ +setName('setup_ldap_discovery'); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('setup', 'LDAP Discovery', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'setup', + 'You can use this page to discover LDAP or ActiveDirectory servers ' . + ' for authentication. If you don\' want to execute a discovery, just skip this step.' + ) + ) + ) + ); + + $this->discoveryForm = new LdapDiscoveryForm(); + $this->addElements($this->discoveryForm->createElements($formData)->getElements()); + $this->getElement('domain')->setRequired( + isset($formData['skip_validation']) === false || ! $formData['skip_validation'] + ); + + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'required' => true, + 'label' => mt('setup', 'Skip'), + 'description' => mt('setup', 'Do not discover LDAP servers and enter all settings manually.') + ) + ); + } + + /** + * Validate the given form data and check whether a BIND-request is successful + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if (! $data['skip_validation'] && false === $this->discoveryForm->isValid($data)) { + return false; + } + + return true; + } + + public function getValues($suppressArrayNotation = false) + { + if (! isset($this->discoveryForm) || ! $this->discoveryForm->hasSuggestion()) { + return null; + } + return array( + 'domain' => $this->getValue('domain'), + 'type' => $this->discoveryForm->isAd() ? + LdapDiscoveryConfirmPage::TYPE_AD : LdapDiscoveryConfirmPage::TYPE_MISC, + 'resource' => $this->discoveryForm->suggestResourceSettings(), + 'backend' => $this->discoveryForm->suggestBackendSettings() + ); + } +} diff --git a/modules/setup/application/forms/LdapResourcePage.php b/modules/setup/application/forms/LdapResourcePage.php new file mode 100644 index 000000000..9f08f75e0 --- /dev/null +++ b/modules/setup/application/forms/LdapResourcePage.php @@ -0,0 +1,121 @@ +setName('setup_ldap_resource'); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + 'hidden', + 'type', + array( + 'required' => true, + 'value' => 'ldap' + ) + ); + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('setup', 'LDAP Resource', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'setup', + 'Now please configure your AD/LDAP resource. This will later ' + . 'be used to authenticate users logging in to Icinga Web 2.' + ) + ) + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + $this->addSkipValidationCheckbox(); + } else { + $this->addElement( + 'hidden', + 'skip_validation', + array( + 'required' => true, + 'value' => 0 + ) + ); + } + + $resourceForm = new LdapResourceForm(); + $this->addElements($resourceForm->createElements($formData)->getElements()); + $this->getElement('name')->setValue('icingaweb_ldap'); + } + + /** + * Validate the given form data and check whether a BIND-request is successful + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) { + if (false === LdapResourceForm::isValidResource($this)) { + $this->addSkipValidationCheckbox(); + return false; + } + } + + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the connection validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'required' => true, + 'label' => mt('setup', 'Skip Validation'), + 'description' => mt( + 'setup', + 'Check this to not to validate connectivity with the given directory service' + ) + ) + ); + } +} diff --git a/modules/setup/application/forms/ModulePage.php b/modules/setup/application/forms/ModulePage.php new file mode 100644 index 000000000..e1e951bcb --- /dev/null +++ b/modules/setup/application/forms/ModulePage.php @@ -0,0 +1,160 @@ +setName('setup_modules'); + $this->setViewScript('form/setup-modules.phtml'); + $this->session = Session::getSession()->getNamespace(get_class($this)); + + $this->modulePaths = array(); + if (($appModulePath = realpath(Icinga::app()->getApplicationDir() . '/../modules')) !== false) { + $this->modulePaths[] = $appModulePath; + } + } + + public function setPageData(array $pageData) + { + $this->pageData = $pageData; + return $this; + } + + public function handleRequest(Request $request = null) + { + if ($this->wasSent($this->getRequestData($request))) { + if (($newModule = $request->getPost('module')) !== null) { + $this->setCurrentModule($newModule); + $this->getResponse()->redirectAndExit($this->getRedirectUrl()); + } else { + // The user submitted this form but with the parent wizard's navigation + // buttons so it's now up to the parent wizard to handle the request.. + } + } else { + $wizard = $this->getCurrentWizard(); + $wizardPage = $wizard->getCurrentPage(); + + $wizard->handleRequest($request); + if ($wizard->isFinished() && $wizardPage->wasSent($wizardPage->getRequestData($request))) { + $wizards = $this->getWizards(); + + $newModule = null; + foreach ($wizards as $moduleName => $moduleWizard) { + if (false === $moduleWizard->isFinished()) { + $newModule = $moduleName; + } + } + + if ($newModule === null) { + // In case all module wizards were completed just pick the first one again + reset($wizards); + $newModule = key($wizards); + } + + $this->setCurrentModule($newModule); + } + } + } + + public function clearSession() + { + $this->session->clear(); + foreach ($this->getWizards() as $wizard) { + $wizard->clearSession(); + } + } + + public function setCurrentModule($moduleName) + { + if (false === array_key_exists($moduleName, $this->getWizards())) { + throw new InvalidArgumentException(sprintf('Module "%s" does not provide a setup wizard', $moduleName)); + } + + $this->session->currentModule = $moduleName; + } + + public function getCurrentModule() + { + $moduleName = $this->session->get('currentModule'); + if ($moduleName === null) { + $moduleName = key($this->getWizards()); + $this->setCurrentModule($moduleName); + } + + return $moduleName; + } + + public function getCurrentWizard() + { + $wizards = $this->getWizards(); + return $wizards[$this->getCurrentModule()]; + } + + public function getModules() + { + if ($this->modules !== null) { + return $this->modules; + } else { + $this->modules = array(); + } + + $moduleManager = Icinga::app()->getModuleManager(); + $moduleManager->detectInstalledModules($this->modulePaths); + foreach ($moduleManager->listInstalledModules() as $moduleName) { + $this->modules[] = $moduleManager->loadModule($moduleName)->getModule($moduleName); + } + + return $this->modules; + } + + public function getWizards() + { + if ($this->wizards !== null) { + return $this->wizards; + } else { + $this->wizards = array(); + } + + foreach ($this->getModules() as $module) { + if ($module->providesSetupWizard()) { + $this->wizards[$module->getName()] = $module->getSetupWizard(); + } + } + + $this->mergePageData($this->wizards); + return $this->wizards; + } + + protected function mergePageData(array $wizards) + { + foreach ($wizards as $wizard) { + $wizardPageData = & $wizard->getPageData(); + foreach ($this->pageData as $pageName => $pageData) { + $wizardPageData[$pageName] = $pageData; + } + } + } +} diff --git a/modules/setup/application/forms/PreferencesPage.php b/modules/setup/application/forms/PreferencesPage.php new file mode 100644 index 000000000..276bbbff9 --- /dev/null +++ b/modules/setup/application/forms/PreferencesPage.php @@ -0,0 +1,85 @@ +setName('setup_preferences_type'); + } + + /** + * Pre-select "db" as preference backend and add a hint to the select element + * + * @return self + */ + public function showDatabaseNote() + { + $this->getElement('type') + ->setValue('db') + ->setDescription( + mt( + 'setup', + 'Note that choosing "Database" causes Icinga Web 2 to use the same database as for authentication.' + ) + ); + return $this; + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'title', + array( + 'value' => mt('setup', 'Preferences', 'setup.page.title'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt('setup', 'Please choose how Icinga Web 2 should store user preferences.') + ) + ) + ); + + $storageTypes = array(); + $storageTypes['ini'] = t('File System (INI Files)'); + if (Platform::extensionLoaded('mysql') || Platform::extensionLoaded('pgsql')) { + $storageTypes['db'] = t('Database'); + } + $storageTypes['null'] = t('Don\'t Store Preferences'); + + $this->addElement( + 'select', + 'type', + array( + 'required' => true, + 'label' => t('User Preference Storage Type'), + 'multiOptions' => $storageTypes + ) + ); + } +} diff --git a/modules/setup/application/forms/RequirementsPage.php b/modules/setup/application/forms/RequirementsPage.php new file mode 100644 index 000000000..076c2eba0 --- /dev/null +++ b/modules/setup/application/forms/RequirementsPage.php @@ -0,0 +1,69 @@ +setName('setup_requirements'); + $this->setViewScript('form/setup-requirements.phtml'); + } + + /** + * Set the requirements to list + * + * @param Requirements $requirements + * + * @return self + */ + public function setRequirements(Requirements $requirements) + { + $this->requirements = $requirements; + return $this; + } + + /** + * Return the requirements to list + * + * @return Requirements + */ + public function getRequirements() + { + return $this->requirements; + } + + /** + * Validate the given form data and check whether the requirements are fulfilled + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + return $this->requirements->fulfilled(); + } +} diff --git a/modules/setup/application/forms/SummaryPage.php b/modules/setup/application/forms/SummaryPage.php new file mode 100644 index 000000000..103a7aadf --- /dev/null +++ b/modules/setup/application/forms/SummaryPage.php @@ -0,0 +1,79 @@ +setName('setup_summary'); + $this->setViewScript('form/setup-summary.phtml'); + } + + /** + * Set the title of what is being set up + * + * @param string $title + */ + public function setSubjectTitle($title) + { + $this->title = $title; + } + + /** + * Return the title of what is being set up + * + * @return string + */ + public function getSubjectTitle() + { + return $this->title; + } + + /** + * Set the summary to show + * + * @param array $summary + * + * @return self + */ + public function setSummary(array $summary) + { + $this->summary = $summary; + return $this; + } + + /** + * Return the summary to show + * + * @return array + */ + public function getSummary() + { + return $this->summary; + } +} diff --git a/modules/setup/application/forms/WelcomePage.php b/modules/setup/application/forms/WelcomePage.php new file mode 100644 index 000000000..8f32280eb --- /dev/null +++ b/modules/setup/application/forms/WelcomePage.php @@ -0,0 +1,45 @@ +setName('setup_welcome'); + $this->setViewScript('form/setup-welcome.phtml'); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + 'text', + 'token', + array( + 'required' => true, + 'label' => mt('setup', 'Setup Token'), + 'description' => mt( + 'setup', + 'For security reasons we need to ensure that you are permitted to run this wizard.' + . ' Please provide a token by following the instructions below.' + ), + 'validators' => array(new TokenValidator(Icinga::app()->getConfigDir() . '/setup.token')) + ) + ); + } +} diff --git a/modules/setup/application/views/scripts/form/setup-admin-account.phtml b/modules/setup/application/views/scripts/form/setup-admin-account.phtml new file mode 100644 index 000000000..ac060526a --- /dev/null +++ b/modules/setup/application/views/scripts/form/setup-admin-account.phtml @@ -0,0 +1,74 @@ +getElement('user_type'); +$showRadioBoxes = strpos(strtolower(get_class($radioElem)), 'radio') !== false; + +?> +
+ getElement('title'); ?> + getElement('description'); ?> +getElement('by_name')) !== null): ?> +
+
+ +
+ +
+ +
+ +
+ +getElement('existing_user')) !== null): ?> +
+
+ +
+ +
+ +
+ +
+ +getElement('new_user')) !== null): ?> +
+
+ + getElement('new_user_password'); ?> + getElement('new_user_2ndpass'); ?> +
+ +
+ +
+ +
+ + + + + getElement($form->getTokenElementName()); ?> + getElement($form->getUidElementName()); ?> +
+ getElement(Wizard::BTN_NEXT); + $btn->setAttrib('class', 'double'); + $btn->setAttrib('tabindex', -1); + echo $btn; + ?> + getElement(Wizard::BTN_PREV); ?> + getElement(Wizard::BTN_NEXT); ?> +
+
\ No newline at end of file diff --git a/modules/setup/application/views/scripts/form/setup-modules.phtml b/modules/setup/application/views/scripts/form/setup-modules.phtml new file mode 100644 index 000000000..9a9b02037 --- /dev/null +++ b/modules/setup/application/views/scripts/form/setup-modules.phtml @@ -0,0 +1,47 @@ + +
+

+

+
+ getElement($form->getTokenElementName()); ?> + getElement($form->getUidElementName()); ?> +
    + + getModules() as $module): ?> + providesSetupWizard()): ?> +
  • + getName() === $form->getCurrentModule(); ?> + + getSetupWizard()->isFinished()): ?> + icon('acknowledgement.png', mt('setup', 'Completed', 'setup.modules.wizard.state')); ?> + + + +
  • + + +
+
+ +

+ +

+ +
+
+ getCurrentWizard()->getForm()->render(); ?> +
+
+ getElement($form->getTokenElementName()); ?> + getElement($form->getUidElementName()); ?> +
+ getElement(Wizard::BTN_PREV); ?> + getElement(Wizard::BTN_NEXT); ?> +
+
\ No newline at end of file diff --git a/modules/setup/application/views/scripts/form/setup-requirements.phtml b/modules/setup/application/views/scripts/form/setup-requirements.phtml new file mode 100644 index 000000000..6d0714994 --- /dev/null +++ b/modules/setup/application/views/scripts/form/setup-requirements.phtml @@ -0,0 +1,44 @@ +getRequirements(); + +?> + + + + + + + + + + + + + + + +

title; ?>

description; ?>message; ?>
+
+ +
+
+
+ getElement($form->getTokenElementName()); ?> + getElement($form->getUidElementName()); ?> +
+ getElement(Wizard::BTN_PREV); ?> + getElement(Wizard::BTN_NEXT); + if (false === $requirements->fulfilled()) { + $btn->setAttrib('disabled', 1); + } + echo $btn; + ?> +
+
\ No newline at end of file diff --git a/modules/setup/application/views/scripts/form/setup-summary.phtml b/modules/setup/application/views/scripts/form/setup-summary.phtml new file mode 100644 index 000000000..714416833 --- /dev/null +++ b/modules/setup/application/views/scripts/form/setup-summary.phtml @@ -0,0 +1,31 @@ + +

getSubjectTitle() +); ?>

+
+getSummary() as $pageHtml): ?> + +
+ +
+ + +
+
+ getElement($form->getTokenElementName()); ?> + getElement($form->getUidElementName()); ?> +
+ getElement(Wizard::BTN_PREV); ?> + getElement(Wizard::BTN_NEXT)->setAttrib('class', 'finish'); ?> +
+
\ No newline at end of file diff --git a/modules/setup/application/views/scripts/form/setup-welcome.phtml b/modules/setup/application/views/scripts/form/setup-welcome.phtml new file mode 100644 index 000000000..64a0af991 --- /dev/null +++ b/modules/setup/application/views/scripts/form/setup-welcome.phtml @@ -0,0 +1,84 @@ +getConfigDir(), '/') . '/setup.token'; +$cliPath = realpath(Icinga::app()->getApplicationDir() . '/../bin/icingacli'); + +?> +
+

+
+

' . mt('setup', 'Icinga Project') . '' + ); ?>

+

+

' + . mt('setup', 'insights', 'setup.welcome.screenshots.label') . '' + ); ?>

+
+ +

+ +

+ +
+ getElement('token'); ?> + getElement($form->getTokenElementName()); ?> + getElement($form->getUidElementName()); ?> +
+ getElement(Wizard::BTN_NEXT); ?> +
+
+
+
+ <?= mt('setup', 'Note'); ?> + Generating a New Setup Token +
+
+

+

+
+ setup config createDirectory ; + setup token create; +
+

+
+ su && mkdir -m 2770 ; + head -c 12 /dev/urandom | base64 | tee ; + chmod 0660 ; +
+

' . mt('setup', 'Icinga Web 2 documentation') . '' // TODO: Add link to iw2 docs which points to the installation topic + ); ?>

+
+
+
\ No newline at end of file diff --git a/modules/setup/application/views/scripts/index/index.phtml b/modules/setup/application/views/scripts/index/index.phtml new file mode 100644 index 000000000..9af37be7c --- /dev/null +++ b/modules/setup/application/views/scripts/index/index.phtml @@ -0,0 +1,145 @@ +getPages(); +$finished = isset($success); +$configPages = array_slice($pages, 2, count($pages) - 4, true); +$currentPos = array_search($wizard->getCurrentPage(), $pages, true); +list($configPagesLeft, $configPagesRight) = array_chunk($configPages, count($configPages) / 2, true); + +$visitedPages = array_keys($wizard->getPageData()); +$maxProgress = @max(array_keys(array_filter( + $pages, + function ($page) use ($visitedPages) { return in_array($page->getName(), $visitedPages); } +))); + +?> + +
+
+ img('img/logo_icinga_big.png'); ?> +
+
+

+ 0 ? 'complete' : ( + $maxProgress > 0 ? 'visited' : 'active' + ); ?> + + + + +
+
+
+

+ 1 ? ' complete' : ( + $maxProgress > 1 ? ' visited' : ( + $currentPos === 1 ? ' active' : '' + ) + ); ?> + + + + +
+
+
+

+ + + + +
+ + $page): ?> + 1 ? ' active' : '') + ); ?> + +
+ +
+ +
+ + +
+
+
+ + $page): ?> + 1 ? ' active' : '') + ); ?> + +
+ +
+ +
+ + +
+
+
+

+ count($pages) - 2 ? ' complete' : ( + $maxProgress > count($pages) - 2 ? ' visited' : ($currentPos === count($pages) - 2 ? ' active' : '') + ); ?> + + + + +
+
+
+

+ + + + + +
+
+
+

+ + + + + +
+
+
+
+
+ + render('index/parts/finish.phtml'); ?> + + render('index/parts/wizard.phtml'); ?> + +
+
\ No newline at end of file diff --git a/modules/setup/application/views/scripts/index/parts/finish.phtml b/modules/setup/application/views/scripts/index/parts/finish.phtml new file mode 100644 index 000000000..94217017d --- /dev/null +++ b/modules/setup/application/views/scripts/index/parts/finish.phtml @@ -0,0 +1,26 @@ +
+
+ + + + +
+ + + + + + +

+ +

+ +
+
+ + + + + +
+
\ No newline at end of file diff --git a/modules/setup/application/views/scripts/index/parts/wizard.phtml b/modules/setup/application/views/scripts/index/parts/wizard.phtml new file mode 100644 index 000000000..94891f93e --- /dev/null +++ b/modules/setup/application/views/scripts/index/parts/wizard.phtml @@ -0,0 +1 @@ +getForm()->render(); ?> \ No newline at end of file diff --git a/modules/setup/library/Setup/Exception/SetupException.php b/modules/setup/library/Setup/Exception/SetupException.php new file mode 100644 index 000000000..da5eed17c --- /dev/null +++ b/modules/setup/library/Setup/Exception/SetupException.php @@ -0,0 +1,16 @@ +requirements[] = $requirement; + return $this; + } + + /** + * Return all registered requirements + * + * @return array + */ + public function getAll() + { + return $this->requirements; + } + + /** + * Return an iterator of all registered requirements + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->getAll()); + } + + /** + * Register an optional requirement + * + * @param string $title + * @param string $description + * @param bool $state + * @param string $message + * + * @return self + */ + public function addOptional($title, $description, $state, $message) + { + $this->add((object) array( + 'title' => $title, + 'message' => $message, + 'description' => $description, + 'state' => (bool) $state ? static::STATE_OK : static::STATE_OPTIONAL + )); + return $this; + } + + /** + * Register a mandatory requirement + * + * @param string $title + * @param string $description + * @param bool $state + * @param string $message + * + * @return self + */ + public function addMandatory($title, $description, $state, $message) + { + $this->add((object) array( + 'title' => $title, + 'message' => $message, + 'description' => $description, + 'state' => (bool) $state ? static::STATE_OK : static::STATE_MANDATORY + )); + return $this; + } + + /** + * Register the given requirements + * + * @param Requirements $requirements The requirements to register + * + * @return self + */ + public function merge(Requirements $requirements) + { + foreach ($requirements->getAll() as $requirement) { + $this->add($requirement); + } + + return $this; + } + + /** + * Make all registered requirements being optional + * + * @return self + */ + public function allOptional() + { + foreach ($this->getAll() as $requirement) { + if ($requirement->state === static::STATE_MANDATORY) { + $requirement->state = static::STATE_OPTIONAL; + } + } + + return $this; + } + + /** + * Return whether all mandatory requirements are fulfilled + * + * @return bool + */ + public function fulfilled() + { + foreach ($this->getAll() as $requirement) { + if ($requirement->state === static::STATE_MANDATORY) { + return false; + } + } + + return true; + } +} diff --git a/modules/setup/library/Setup/Setup.php b/modules/setup/library/Setup/Setup.php new file mode 100644 index 000000000..ec5117c9a --- /dev/null +++ b/modules/setup/library/Setup/Setup.php @@ -0,0 +1,96 @@ +steps = array(); + } + + public function getIterator() + { + 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 HTML strings + */ + public function getReport() + { + $reports = array(); + foreach ($this->steps as $step) { + $reports[] = $step->getReport(); + } + + return $reports; + } +} diff --git a/modules/setup/library/Setup/SetupWizard.php b/modules/setup/library/Setup/SetupWizard.php new file mode 100644 index 000000000..754207276 --- /dev/null +++ b/modules/setup/library/Setup/SetupWizard.php @@ -0,0 +1,25 @@ +data = $data; + } + + public function apply() + { + $success = $this->createAuthenticationIni(); + if (isset($this->data['adminAccountData']['resourceConfig'])) { + $success &= $this->createAccount(); + } + + $success &= $this->defineInitialAdmin(); + 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 { + $writer = new IniWriter(array( + 'config' => new Config($config), + 'filename' => Config::resolvePath('authentication.ini') + )); + $writer->write(); + } catch (Exception $e) { + $this->authIniError = $e; + return false; + } + + $this->authIniError = false; + return true; + } + + protected function defineInitialAdmin() + { + $config = array(); + $config['admins'] = array( + 'users' => $this->data['adminAccountData']['username'], + 'permission' => '*' + ); + + try { + $writer = new IniWriter(array( + 'config' => new Config($config), + 'filename' => Config::resolvePath('permissions.ini') + )); + $writer->write(); + } catch (Exception $e) { + $this->permIniError = $e; + return false; + } + + $this->permIniError = false; + return true; + } + + protected function createAccount() + { + try { + $backend = new DbUserBackend( + ResourceFactory::createResource(new Config($this->data['adminAccountData']['resourceConfig'])) + ); + + if (array_search($this->data['adminAccountData']['username'], $backend->listUsers()) === false) { + $backend->addUser( + $this->data['adminAccountData']['username'], + $this->data['adminAccountData']['password'] + ); + } + } catch (Exception $e) { + $this->dbError = $e; + return false; + } + + $this->dbError = false; + return true; + } + + public function getSummary() + { + $pageTitle = '

' . mt('setup', 'Authentication', 'setup.page.title') . '

'; + $backendTitle = '

' . mt('setup', 'Authentication Backend', 'setup.page.title') . '

'; + $adminTitle = '

' . mt('setup', 'Administration', 'setup.page.title') . '

'; + + $authType = $this->data['backendConfig']['backend']; + $backendDesc = '

' . sprintf( + mt('setup', 'Users will authenticate using %s.', 'setup.summary.auth'), + $authType === 'db' ? mt('setup', 'a database', 'setup.summary.auth.type') : ( + $authType === 'ldap' ? 'LDAP' : mt('setup', 'webserver authentication', 'setup.summary.auth.type') + ) + ) . '

'; + + $backendHtml = '' + . '' + . '' + . '' + . '' + . '' + . '' + . ($authType === 'ldap' ? ( + '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + ) : ($authType === 'autologin' ? ( + '' + . '' + . '' + . '' + ) : '')) + . '' + . '
' . t('Backend Name') . '' . $this->data['backendConfig']['name'] . '
' . t('User Object Class') . '' . $this->data['backendConfig']['user_class'] . '
' . t('User Name Attribute') . '' . $this->data['backendConfig']['user_name_attribute'] . '
' . t('Filter Pattern') . '' . $this->data['backendConfig']['strip_username_regexp'] . '
'; + + $adminHtml = '

' . (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'] + )) . '

'; + + return $pageTitle . '
' . $backendDesc . $backendTitle . $backendHtml . '
' + . '
' . $adminTitle . $adminHtml . '
'; + } + + public function getReport() + { + $report = ''; + if ($this->authIniError === false) { + $message = mt('setup', 'Authentication configuration has been successfully written to: %s'); + $report .= '

' . sprintf($message, Config::resolvePath('authentication.ini')) . '

'; + } elseif ($this->authIniError !== null) { + $message = mt('setup', 'Authentication configuration could not be written to: %s; An error occured:'); + $report .= '

' . sprintf($message, Config::resolvePath('authentication.ini')) . '

' + . '

' . $this->authIniError->getMessage() . '

'; + } + + if ($this->dbError === false) { + $message = mt('setup', 'Account "%s" has been successfully created.'); + $report .= '

' . sprintf($message, $this->data['adminAccountData']['username']) . '

'; + } elseif ($this->dbError !== null) { + $message = mt('setup', 'Unable to create account "%s". An error occured:'); + $report .= '

' . sprintf($message, $this->data['adminAccountData']['username']) . '

' + . '

' . $this->dbError->getMessage() . '

'; + } + + if ($this->permIniError === false) { + $message = mt('setup', 'Account "%s" has been successfully defined as initial administrator.'); + $report .= '

' . sprintf($message, $this->data['adminAccountData']['username']) . '

'; + } elseif ($this->permIniError !== null) { + $message = mt('setup', 'Unable to define account "%s" as initial administrator. An error occured:'); + $report .= '

' . sprintf($message, $this->data['adminAccountData']['username']) . '

' + . '

' . $this->permIniError->getMessage() . '

'; + } + + 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 000000000..c6ec2ef2b --- /dev/null +++ b/modules/setup/library/Setup/Steps/DatabaseStep.php @@ -0,0 +1,265 @@ +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(key($this->data['tables']), $db->listTables()) !== false) { + $this->log(mt('setup', 'Database schema already exists...')); + } else { + $this->log(mt('setup', 'Creating database schema...')); + $db->import(Icinga::app()->getApplicationDir() . '/../etc/schema/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(key($this->data['tables']), $db->listTables()) !== false) { + $this->log(mt('setup', 'Database schema already exists...')); + } else { + $this->log(mt('setup', 'Creating database schema...')); + $db->import(Icinga::app()->getApplicationDir() . '/../etc/schema/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(key($this->data['tables']), $db->listTables()) === 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 '

' . mt('setup', 'Database Setup', 'setup.page.title') . '

' . $message . '

'; + } + + public function getReport() + { + if ($this->error === false) { + return '

' . join('

', $this->messages) . '

' + . '

' . mt('setup', 'The database has been fully set up!') . '

'; + } elseif ($this->error !== null) { + $message = mt('setup', 'Failed to fully setup the database. An error occured:'); + return '

' . join('

', $this->messages) . '

' + . '

' . $message . '

' . $this->error->getMessage() . '

'; + } + } + + 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 000000000..6a3f12897 --- /dev/null +++ b/modules/setup/library/Setup/Steps/GeneralConfigStep.php @@ -0,0 +1,122 @@ +data = $data; + } + + public function apply() + { + $config = array(); + foreach ($this->data['generalConfig'] as $sectionAndPropertyName => $value) { + list($section, $property) = explode('_', $sectionAndPropertyName); + $config[$section][$property] = $value; + } + + $config['preferences']['type'] = $this->data['preferencesType']; + if (isset($this->data['preferencesResource'])) { + $config['preferences']['resource'] = $this->data['preferencesResource']; + } + + try { + $writer = new IniWriter(array( + 'config' => new Config($config), + 'filename' => Config::resolvePath('config.ini') + )); + $writer->write(); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + $pageTitle = '

' . mt('setup', 'Application Configuration', 'setup.page.title') . '

'; + $generalTitle = '

' . t('General', 'app.config') . '

'; + $loggingTitle = '

' . t('Logging', 'app.config') . '

'; + + $generalHtml = '' + . ''; + + $type = $this->data['generalConfig']['logging_log']; + if ($type === 'none') { + $loggingHtml = '

' . mt('setup', 'Logging will be disabled.') . '

'; + } else { + $level = $this->data['generalConfig']['logging_level']; + $loggingHtml = '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . ($type === 'syslog' ? ( + '' + . '' + ) : ( + '' + . '' + )) + . '' + . '' + . '
' . t('Type', 'app.config.logging') . '' . ($type === 'syslog' ? 'Syslog' : t('File', 'app.config.logging.type')) . '
' . t('Level', 'app.config.logging') . '' . ($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') + ) + ) + )) . '
' . t('Application Prefix') . '' . $this->data['generalConfig']['logging_application'] . '' . t('Filepath') . '' . $this->data['generalConfig']['logging_file'] . '
'; + } + + return $pageTitle . '
' . $generalTitle . $generalHtml . '
' + . '
' . $loggingTitle . $loggingHtml . '
'; + } + + public function getReport() + { + if ($this->error === false) { + $message = mt('setup', 'General configuration has been successfully written to: %s'); + return '

' . sprintf($message, Config::resolvePath('config.ini')) . '

'; + } elseif ($this->error !== null) { + $message = mt('setup', 'General configuration could not be written to: %s; An error occured:'); + return '

' . sprintf($message, Config::resolvePath('config.ini')) . '

' + . '

' . $this->error->getMessage() . '

'; + } + } +} diff --git a/modules/setup/library/Setup/Steps/ResourceStep.php b/modules/setup/library/Setup/Steps/ResourceStep.php new file mode 100644 index 000000000..75e79a8fa --- /dev/null +++ b/modules/setup/library/Setup/Steps/ResourceStep.php @@ -0,0 +1,149 @@ +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 { + $writer = new IniWriter(array( + 'config' => new Config($resourceConfig), + 'filename' => Config::resolvePath('resources.ini'), + 'filemode' => 0660 + )); + $writer->write(); + } 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 = '

' . mt('setup', 'Resources', 'setup.page.title') . '

'; + } else { + $pageTitle = '

' . mt('setup', 'Resource', 'setup.page.title') . '

'; + } + + if (isset($this->data['dbResourceConfig'])) { + $dbTitle = '

' . mt('setup', 'Database', 'setup.page.title') . '

'; + $dbHtml = '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
' . t('Resource Name') . '' . $this->data['dbResourceConfig']['name'] . '
' . t('Database Type') . '' . $this->data['dbResourceConfig']['db'] . '
' . t('Host') . '' . $this->data['dbResourceConfig']['host'] . '
' . t('Port') . '' . $this->data['dbResourceConfig']['port'] . '
' . t('Database Name') . '' . $this->data['dbResourceConfig']['dbname'] . '
' . t('Username') . '' . $this->data['dbResourceConfig']['username'] . '
' . t('Password') . '' . str_repeat('*', strlen($this->data['dbResourceConfig']['password'])) . '
'; + } + + if (isset($this->data['ldapResourceConfig'])) { + $ldapTitle = '

LDAP

'; + $ldapHtml = '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
' . t('Resource Name') . '' . $this->data['ldapResourceConfig']['name'] . '
' . t('Host') . '' . $this->data['ldapResourceConfig']['hostname'] . '
' . t('Port') . '' . $this->data['ldapResourceConfig']['port'] . '
' . t('Root DN') . '' . $this->data['ldapResourceConfig']['root_dn'] . '
' . t('Bind DN') . '' . $this->data['ldapResourceConfig']['bind_dn'] . '
' . t('Bind Password') . '' . str_repeat('*', strlen($this->data['ldapResourceConfig']['bind_pw'])) . '
'; + } + + return $pageTitle . (isset($dbTitle) ? '
' . $dbTitle . $dbHtml . '
' : '') + . (isset($ldapTitle) ? '
' . $ldapTitle . $ldapHtml . '
' : ''); + } + + public function getReport() + { + if ($this->error === false) { + $message = mt('setup', 'Resource configuration has been successfully written to: %s'); + return '

' . sprintf($message, Config::resolvePath('resources.ini')) . '

'; + } elseif ($this->error !== null) { + $message = mt('setup', 'Resource configuration could not be written to: %s; An error occured:'); + return '

' . sprintf($message, Config::resolvePath('resources.ini')) . '

' + . '

' . $this->error->getMessage() . '

'; + } + } +} diff --git a/modules/setup/library/Setup/Utils/DbTool.php b/modules/setup/library/Setup/Utils/DbTool.php new file mode 100644 index 000000000..344b98d8a --- /dev/null +++ b/modules/setup/library/Setup/Utils/DbTool.php @@ -0,0 +1,781 @@ + 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' => 0, + '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) + { + $this->config = $config; + } + + /** + * Connect to the server + * + * @return self + */ + 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 self + */ + 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 (false === isset($this->config['db'])) { + throw new ConfigurationError('Can\'t connect to database server of unknown type'); + } elseif (false === isset($this->config['host'])) { + throw new ConfigurationError('Can\'t connect to database server without a hostname or address'); + } elseif (false === isset($this->config['port'])) { + throw new ConfigurationError('Can\'t connect to database server without a port'); + } elseif (false === isset($this->config['username'])) { + throw new ConfigurationError('Can\'t connect to database server without a username'); + } elseif (false === 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 (false === 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'); + } + } + + /** + * 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 + */ + protected function _zendConnect($dbname) + { + if ($this->zendConn !== null) { + return; + } + + $config = array( + 'dbname' => $dbname, + 'username' => $this->config['username'], + 'password' => $this->config['password'] + ); + + if ($this->config['db'] === 'mysql') { + $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'] + ); + } + } + + /** + * Initialize PDO connection + * + * @param string $dbname The name of the database to connect with + */ + protected function _pdoConnect($dbname) + { + if ($this->pdoConn !== null) { + return; + } + + $this->pdoConn = new PDO( + $this->buildDsn($this->config['db'], $dbname), + $this->config['username'], + $this->config['password'], + array(PDO::ATTR_TIMEOUT => 1, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION) + ); + } + + /** + * 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(); + if ($code !== 1040 && $code !== 1045) { + 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 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; + } + + /** + * 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()); + $queryString = sprintf( + 'GRANT %%s ON %s.%%s TO %s@%s', + $this->quoteIdentifier($this->config['dbname']), + $this->quoteIdentifier($username), + $this->quoteIdentifier($host) + ); + + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->mysqlGrantContexts)) as $privilege) { + if (false === empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (false === empty($tablePrivileges)) { + foreach ($context as $table) { + $this->exec( + sprintf($queryString, join(',', $tablePrivileges), $this->quoteIdentifier($table)) + ); + } + } + + if (false === empty($dbPrivileges)) { + $this->exec(sprintf($queryString, join(',', $dbPrivileges), '*')); + } + } elseif ($this->config['db'] === 'pgsql') { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) { + if (false === empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (false === empty($dbPrivileges)) { + $this->exec(sprintf( + 'GRANT %s ON DATABASE %s TO %s', + join(',', $dbPrivileges), + $this->config['dbname'], + $username + )); + } + + if (false === 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 = <<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 . "'"; + $privilegeCondition = sprintf( + 'privilege_type IN (%s)', + join(',', array_map(array($this, 'quote'), $mysqlPrivileges)) + ); + + if (isset($this->config['dbname'])) { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach ($mysqlPrivileges as $privilege) { + if (false === empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + $dbPrivilegesGranted = true; + if (false === empty($dbPrivileges)) { + $query = $this->query( + 'SELECT COUNT(*) as matches' + . ' FROM information_schema.schema_privileges' + . ' WHERE grantee = :grantee' + . ' AND table_schema = :dbname' + . ' AND ' . $privilegeCondition + . ($requireGrants ? " AND is_grantable = 'YES'" : ''), + array(':grantee' => $grantee, ':dbname' => $this->config['dbname']) + ); + $dbPrivilegesGranted = (int) $query->fetchObject()->matches === count($dbPrivileges); + } + + $tablePrivilegesGranted = true; + if (false === empty($tablePrivileges)) { + $tableCondition = 'table_name IN (' . join(',', array_map(array($this, 'quote'), $context)) . ')'; + $query = $this->query( + 'SELECT COUNT(*) as matches' + . ' FROM information_schema.table_privileges' + . ' WHERE grantee = :grantee' + . ' AND table_schema = :dbname' + . ' AND ' . $tableCondition + . ' AND ' . $privilegeCondition + . ($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 ' . $privilegeCondition . ($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 (false === empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (false === empty($dbPrivileges)) { + $query = $this->query( + 'SELECT has_database_privilege(:user, :dbname, :privileges) AS db_privileges_granted', + array( + ':user' => $username !== null ? $username : $this->config['username'], + ':dbname' => $this->config['dbname'], + ':privileges' => join(',', $dbPrivileges) . ($requireGrants ? ' WITH GRANT OPTION' : '') + ) + ); + $privilegesGranted &= $query->fetchObject()->db_privileges_granted; + } + + if (false === empty($tablePrivileges)) { + foreach (array_intersect($context, $this->listTables()) as $table) { + $query = $this->query( + 'SELECT has_table_privilege(:user, :table, :privileges) AS table_privileges_granted', + array( + ':user' => $username !== null ? $username : $this->config['username'], + ':table' => $table, + ':privileges' => join(',', $tablePrivileges) . ($requireGrants ? ' WITH GRANT OPTION' : '') + ) + ); + $privilegesGranted &= $query->fetchObject()->table_privileges_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) !== 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) !== 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) !== 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 $privilegesGranted; + } +} diff --git a/modules/setup/library/Setup/Utils/EnableModuleStep.php b/modules/setup/library/Setup/Utils/EnableModuleStep.php new file mode 100644 index 000000000..3ca3eebd1 --- /dev/null +++ b/modules/setup/library/Setup/Utils/EnableModuleStep.php @@ -0,0 +1,59 @@ +moduleName = $moduleName; + + $this->modulePaths = array(); + if (($appModulePath = realpath(Icinga::app()->getApplicationDir() . '/../modules')) !== false) { + $this->modulePaths[] = $appModulePath; + } + } + + public function apply() + { + try { + $moduleManager = Icinga::app()->getModuleManager(); + $moduleManager->detectInstalledModules($this->modulePaths); + $moduleManager->enableModule($this->moduleName); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + // Enabling a module is like a implicit action, which does not need to be shown to the user... + } + + public function getReport() + { + if ($this->error === false) { + return '

' . sprintf(mt('setup', 'Module "%s" has been successfully enabled.'), $this->moduleName) . '

'; + } elseif ($this->error !== null) { + $message = mt('setup', 'Module "%s" could not be enabled. An error occured:'); + return '

' . sprintf($message, $this->moduleName) . '

' + . '

' . $this->error->getMessage() . '

'; + } + } +} diff --git a/modules/setup/library/Setup/Utils/MakeDirStep.php b/modules/setup/library/Setup/Utils/MakeDirStep.php new file mode 100644 index 000000000..d15c7db6a --- /dev/null +++ b/modules/setup/library/Setup/Utils/MakeDirStep.php @@ -0,0 +1,73 @@ +paths = $paths; + $this->dirmode = $dirmode; + $this->errors = array(); + } + + public function apply() + { + $success = true; + foreach ($this->paths as $path) { + if (false === file_exists($path)) { + if (false === @mkdir($path)) { + $this->errors[$path] = error_get_last(); + $success = false; + } else { + $this->errors[$path] = null; + $old = umask(0); + chmod($path, $this->dirmode); + umask($old); + } + } + } + + return $success; + } + + public function getSummary() + { + // This step is usually being used for directories which are required for the configuration but + // are not defined in any way by the user. So there is no need to show a summary for this step. + } + + public function getReport() + { + $okMessage = mt('setup', 'Directory "%s" in "%s" has been successfully created.'); + $failMessage = mt('setup', 'Unable to create directory "%s" in "%s". An error occured:'); + + $report = ''; + foreach ($this->paths as $path) { + if (array_key_exists($path, $this->errors)) { + if (is_array($this->errors[$path])) { + $report .= '

' . sprintf($failMessage, basename($path), dirname($path)) . '

' + . '

' . $this->errors[$path]['message'] . '

'; + } else { + $report .= '

' . sprintf($okMessage, basename($path), dirname($path)) . '

'; + } + } + } + + 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 000000000..7dc2ea2f9 --- /dev/null +++ b/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php @@ -0,0 +1,84 @@ +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_FILE_PUBLIC' => sprintf( + mt('setup', 'Cannot validate token, file "%s" must only be accessible by the webserver\'s user.'), + $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) + { + $tokenStats = @stat($this->tokenPath); + if (($tokenStats['mode'] & 4) === 4) { + $this->_error('TOKEN_FILE_PUBLIC'); + return false; + } + + 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 000000000..9c5ec76e7 --- /dev/null +++ b/modules/setup/library/Setup/WebWizard.php @@ -0,0 +1,571 @@ +addPage(new WelcomePage()); + $this->addPage(new RequirementsPage()); + $this->addPage(new AuthenticationPage()); + $this->addPage(new PreferencesPage()); + $this->addPage(new DbResourcePage()); + $this->addPage(new LdapDiscoveryPage()); + $this->addPage(new LdapDiscoveryConfirmPage()); + $this->addPage(new LdapResourcePage()); + $this->addPage(new AuthBackendPage()); + $this->addPage(new AdminAccountPage()); + $this->addPage(new GeneralConfigPage()); + $this->addPage(new DatabaseCreationPage()); + $this->addPage(new ModulePage()); + $this->addPage(new SummaryPage()); + } + + /** + * @see Wizard::setupPage() + */ + public function setupPage(Form $page, Request $request) + { + if ($page->getName() === 'setup_requirements') { + $page->setRequirements($this->getRequirements()); + } elseif ($page->getName() === 'setup_preferences_type') { + $authData = $this->getPageData('setup_authentication_type'); + if ($authData['type'] === 'db') { + $page->create()->showDatabaseNote(); + } + } elseif ($page->getName() === 'setup_authentication_backend') { + $authData = $this->getPageData('setup_authentication_type'); + if ($authData['type'] === 'db') { + $page->setResourceConfig($this->getPageData('setup_db_resource')); + } elseif ($authData['type'] === 'ldap') { + $page->setResourceConfig($this->getPageData('setup_ldap_resource')); + + $suggestions = $this->getPageData('setup_ldap_discovery_confirm'); + if (isset($suggestions['backend'])) { + $page->populate($suggestions['backend']); + } + } + } elseif ($page->getName() === 'setup_ldap_discovery_confirm') { + $page->setResourceConfig($this->getPageData('setup_ldap_discovery')); + } elseif ($page->getName() === 'setup_admin_account') { + $page->setBackendConfig($this->getPageData('setup_authentication_backend')); + $authData = $this->getPageData('setup_authentication_type'); + if ($authData['type'] === 'db') { + $page->setResourceConfig($this->getPageData('setup_db_resource')); + } elseif ($authData['type'] === 'ldap') { + $page->setResourceConfig($this->getPageData('setup_ldap_resource')); + } + } elseif ($page->getName() === 'setup_database_creation') { + $page->setDatabaseSetupPrivileges($this->databaseSetupPrivileges); + $page->setDatabaseUsagePrivileges($this->databaseUsagePrivileges); + $page->setResourceConfig($this->getPageData('setup_db_resource')); + } elseif ($page->getName() === 'setup_summary') { + $page->setSubjectTitle('Icinga Web 2'); + $page->setSummary($this->getSetup()->getSummary()); + } elseif ($page->getName() === 'setup_db_resource') { + $ldapData = $this->getPageData('setup_ldap_resource'); + if ($ldapData !== null && $request->getPost('name') === $ldapData['name']) { + $page->addError( + mt('setup', 'The given resource name must be unique and is already in use by the LDAP resource') + ); + } + } elseif ($page->getName() === 'setup_ldap_resource') { + $dbData = $this->getPageData('setup_db_resource'); + if ($dbData !== null && $request->getPost('name') === $dbData['name']) { + $page->addError( + mt('setup', 'The given resource name must be unique and is already in use by the database resource') + ); + } + + $suggestion = $this->getPageData('setup_ldap_discovery_confirm'); + if (isset($suggestion['resource'])) { + $page->populate($suggestion['resource']); + } + } elseif ($page->getName() === 'setup_authentication_type' && $this->getDirection() === static::FORWARD) { + $authData = $this->getPageData($page->getName()); + 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 + $pageData = & $this->getPageData(); + unset($pageData['setup_admin_account']); + unset($pageData['setup_authentication_backend']); + } + } elseif ($page->getName() === 'setup_modules') { + $page->setPageData($this->getPageData()); + $page->handleRequest($request); + } + } + + /** + * @see Wizard::getNewPage() + */ + protected function getNewPage($requestedPage, Form $originPage) + { + $skip = false; + $newPage = parent::getNewPage($requestedPage, $originPage); + if ($newPage->getName() === 'setup_db_resource') { + $prefData = $this->getPageData('setup_preferences_type'); + $authData = $this->getPageData('setup_authentication_type'); + $skip = $prefData['type'] !== 'db' && $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_database_creation') { + if (($config = $this->getPageData('setup_db_resource')) !== null && ! $config['skip_validation']) { + $db = new DbTool($config); + + try { + $db->connectToDb(); // Are we able to login on the database? + if (array_search(key($this->databaseTables), $db->listTables()) === false) { + // In case the database schema does not yet exist the user + // needs the privileges to create and 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($this->databaseSetupPrivileges, $this->databaseTables); + } catch (PDOException $_) { + // We are NOT able to login on the server.. + } + } + } else { + $skip = true; + } + } + + if ($skip) { + if ($this->hasPageData($newPage->getName())) { + $pageData = & $this->getPageData(); + unset($pageData[$newPage->getName()]); + } + + $pages = $this->getPages(); + if ($this->getDirection() === static::FORWARD) { + $nextPage = $pages[array_search($newPage, $pages, true) + 1]; + $newPage = $this->getNewPage($nextPage->getName(), $newPage); + } else { // $this->getDirection() === static::BACKWARD + $previousPage = $pages[array_search($newPage, $pages, true) - 1]; + $newPage = $this->getNewPage($previousPage->getName(), $newPage); + } + } + + return $newPage; + } + + /** + * @see Wizard::addButtons() + */ + 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')); + } + } + + /** + * @see Wizard::clearSession() + */ + public function clearSession() + { + parent::clearSession(); + $this->getPage('setup_modules')->clearSession(); + + $tokenPath = Config::resolvePath('setup.token'); + if (file_exists($tokenPath)) { + @unlink($tokenPath); + } + } + + /** + * @see SetupWizard::getSetup() + */ + public function getSetup() + { + $pageData = $this->getPageData(); + $setup = new Setup(); + + if (isset($pageData['setup_db_resource']) + && ! $pageData['setup_db_resource']['skip_validation'] + && (false === isset($pageData['setup_database_creation']) + || ! $pageData['setup_database_creation']['skip_validation'] + ) + ) { + $setup->addStep( + new DatabaseStep(array( + 'tables' => $this->databaseTables, + 'privileges' => $this->databaseUsagePrivileges, + 'resourceConfig' => $pageData['setup_db_resource'], + 'adminName' => isset($pageData['setup_database_creation']['username']) + ? $pageData['setup_database_creation']['username'] + : null, + 'adminPassword' => isset($pageData['setup_database_creation']['password']) + ? $pageData['setup_database_creation']['password'] + : null + )) + ); + } + + $setup->addStep( + new GeneralConfigStep(array( + 'generalConfig' => $pageData['setup_general_config'], + 'preferencesType' => $pageData['setup_preferences_type']['type'], + 'preferencesResource' => isset($pageData['setup_db_resource']['name']) + ? $pageData['setup_db_resource']['name'] + : null + )) + ); + + $adminAccountType = $pageData['setup_admin_account']['user_type']; + $adminAccountData = array('username' => $pageData['setup_admin_account'][$adminAccountType]); + if ($adminAccountType === 'new_user' && ! $pageData['setup_db_resource']['skip_validation'] + && (false === isset($pageData['setup_database_creation']) + || ! $pageData['setup_database_creation']['skip_validation'] + ) + ) { + $adminAccountData['resourceConfig'] = $pageData['setup_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_db_resource']['name'] : ( + $authType === 'ldap' ? $pageData['setup_ldap_resource']['name'] : null + ) + )) + ); + + if (isset($pageData['setup_db_resource']) || isset($pageData['setup_ldap_resource'])) { + $setup->addStep( + new ResourceStep(array( + 'dbResourceConfig' => isset($pageData['setup_db_resource']) + ? array_diff_key($pageData['setup_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 + )) + ); + } + + $configDir = $this->getConfigDir(); + $setup->addStep( + new MakeDirStep( + array( + $configDir . '/modules', + $configDir . '/preferences', + $configDir . '/enabledModules' + ), + 0775 + ) + ); + + foreach ($this->getPage('setup_modules')->setPageData($this->getPageData())->getWizards() as $wizard) { + if ($wizard->isFinished()) { + $setup->addSteps($wizard->getSetup()->getSteps()); + } + } + + return $setup; + } + + /** + * @see SetupWizard::getRequirements() + */ + public function getRequirements() + { + $requirements = new Requirements(); + + $phpVersion = Platform::getPhpVersion(); + $requirements->addMandatory( + mt('setup', 'PHP Version'), + mt( + 'setup', + 'Running Icinga Web 2 requires PHP version 5.3.2. Advanced features' + . ' like the built-in web server require PHP version 5.4.' + ), + version_compare($phpVersion, '5.3.2', '>='), + sprintf(mt('setup', 'You are running PHP version %s.'), $phpVersion) + ); + + // The only reason for requiring 1.12.2 is a bug in Zend_Form_Decorator_ViewHelper in older versions causing a + // Zend_Form_Element_Button's value not being rendered. Feel free to adjust this in case we require an earlier + // version! + $zendVersion = Zend_Version::VERSION; + $requirements->addMandatory( + mt('setup', 'Zend Framework 1'), + mt( + 'setup', + 'Icinga Web 2 requires at least Zend Framework 1.12.2 to work properly.' + ), + Zend_Version::compareVersion('1.12.2') !== 1, + sprintf(mt('setup', 'You have got Zend Framwork %s installed.'), $zendVersion) + ); + + $defaultTimezone = Platform::getPhpConfig('date.timezone'); + $requirements->addMandatory( + mt('setup', 'Default Timezone'), + mt('setup', 'It is required that a default timezone has been set using date.timezone in php.ini.'), + $defaultTimezone, + $defaultTimezone ? sprintf(mt('setup', 'Your default timezone is: %s'), $defaultTimezone) : ( + mt('setup', 'You did not define a default timezone.') + ) + ); + + $requirements->addOptional( + mt('setup', 'Linux Platform'), + 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.' + ), + Platform::isLinux(), + sprintf(mt('setup', 'You are running PHP on a %s system.'), Platform::getOperatingSystemName()) + ); + + $requirements->addMandatory( + mt('setup', 'PHP Module: OpenSSL'), + mt('setup', 'The PHP module for OpenSSL is required to generate cryptographically safe password salts.'), + Platform::extensionLoaded('openssl'), + Platform::extensionLoaded('openssl') ? mt('setup', 'The PHP module for OpenSSL is available.') : ( + mt('setup', 'The PHP module for OpenSSL is missing.') + ) + ); + + $requirements->addOptional( + mt('setup', 'PHP Module: JSON'), + mt('setup', 'The JSON module for PHP is required for various export functionalities as well as APIs.'), + Platform::extensionLoaded('json'), + Platform::extensionLoaded('json') ? mt('setup', 'The PHP module JSON is available.') : ( + mt('setup', 'The PHP module JSON is missing.') + ) + ); + + $requirements->addOptional( + mt('setup', 'PHP Module: LDAP'), + mt('setup', 'If you\'d like to authenticate users using LDAP the corresponding PHP module is required'), + Platform::extensionLoaded('ldap'), + Platform::extensionLoaded('ldap') ? mt('setup', 'The PHP module LDAP is available') : ( + mt('setup', 'The PHP module LDAP is missing') + ) + ); + + $requirements->addOptional( + mt('setup', 'PHP Module: INTL'), + mt( + 'setup', + 'If you want your users to benefit from language, timezone and date/time' + . ' format negotiation, the INTL module for PHP is required.' + ), + Platform::extensionLoaded('intl'), + Platform::extensionLoaded('intl') ? mt('setup', 'The PHP module INTL is available') : ( + mt('setup', 'The PHP module INTL is missing') + ) + ); + + // TODO(6172): Remove this requirement once we do not ship dompdf with Icinga Web 2 anymore + $requirements->addOptional( + mt('setup', 'PHP Module: DOM'), + mt('setup', 'To be able to export views and reports to PDF, the DOM module for PHP is required.'), + Platform::extensionLoaded('dom'), + Platform::extensionLoaded('dom') ? mt('setup', 'The PHP module DOM is available') : ( + mt('setup', 'The PHP module DOM is missing') + ) + ); + + $requirements->addOptional( + mt('setup', 'PHP Module: GD'), + mt( + 'setup', + 'In case you want icons and graphs being exported to PDF' + . ' as well, you\'ll need the GD extension for PHP.' + ), + Platform::extensionLoaded('gd'), + Platform::extensionLoaded('gd') ? mt('setup', 'The PHP module GD is available') : ( + mt('setup', 'The PHP module GD is missing') + ) + ); + + $requirements->addOptional( + mt('setup', 'PHP Module: PDO-MySQL'), + mt( + 'setup', + 'Is Icinga Web 2 supposed to access a MySQL database the PDO-MySQL module for PHP is required.' + ), + Platform::extensionLoaded('mysql'), + Platform::extensionLoaded('mysql') ? mt('setup', 'The PHP module PDO-MySQL is available.') : ( + mt('setup', 'The PHP module PDO-MySQL is missing.') + ) + ); + + $requirements->addOptional( + mt('setup', 'PHP Module: PDO-PostgreSQL'), + mt( + 'setup', + 'Is Icinga Web 2 supposed to access a PostgreSQL database' + . ' the PDO-PostgreSQL module for PHP is required.' + ), + Platform::extensionLoaded('pgsql'), + Platform::extensionLoaded('pgsql') ? mt('setup', 'The PHP module PDO-PostgreSQL is available.') : ( + mt('setup', 'The PHP module PDO-PostgreSQL is missing.') + ) + ); + + // TODO(7464): Re-enable or remove this entirely once a decision has been made regarding shipping Zend with Iw2 + /*$mysqlAdapterFound = Platform::zendClassExists('Zend_Db_Adapter_Pdo_Mysql'); + $requirements->addOptional( + mt('setup', 'Zend Database Adapter For MySQL'), + mt('setup', 'The Zend database adapter for MySQL is required to access a MySQL database.'), + $mysqlAdapterFound, + $mysqlAdapterFound ? mt('setup', 'The Zend database adapter for MySQL is available.') : ( + mt('setup', 'The Zend database adapter for MySQL is missing.') + ) + ); + + $pgsqlAdapterFound = Platform::zendClassExists('Zend_Db_Adapter_Pdo_Pgsql'); + $requirements->addOptional( + mt('setup', 'Zend Database Adapter For PostgreSQL'), + mt('setup', 'The Zend database adapter for PostgreSQL is required to access a PostgreSQL database.'), + $pgsqlAdapterFound, + $pgsqlAdapterFound ? mt('setup', 'The Zend database adapter for PostgreSQL is available.') : ( + mt('setup', 'The Zend database adapter for PostgreSQL is missing.') + ) + );*/ + + $configDir = $this->getConfigDir(); + $requirements->addMandatory( + mt('setup', 'Writable Config Directory'), + mt( + 'setup', + 'The Icinga Web 2 configuration directory defaults to "/etc/icingaweb", if' . + ' not explicitly set in the environment variable "ICINGAWEB_CONFIGDIR".' + ), + is_writable($configDir), + sprintf( + is_writable($configDir) ? mt('setup', 'The current configuration directory is writable: %s') : ( + mt('setup', 'The current configuration directory is not writable: %s') + ), + $configDir + ) + ); + + foreach ($this->getPage('setup_modules')->setPageData($this->getPageData())->getWizards() as $wizard) { + $requirements->merge($wizard->getRequirements()->allOptional()); + } + + return $requirements; + } + + /** + * Return the configuration directory of Icinga Web 2 + * + * @return string + */ + protected function getConfigDir() + { + if (array_key_exists('ICINGAWEB_CONFIGDIR', $_SERVER)) { + $configDir = $_SERVER['ICINGAWEB_CONFIGDIR']; + } else { + $configDir = '/etc/icingaweb'; + } + + $canonical = realpath($configDir); + return $canonical ? $canonical : $configDir; + } +} diff --git a/modules/test/application/clicommands/PhpCommand.php b/modules/test/application/clicommands/PhpCommand.php index 126e0c462..dbcb58f05 100644 --- a/modules/test/application/clicommands/PhpCommand.php +++ b/modules/test/application/clicommands/PhpCommand.php @@ -56,7 +56,7 @@ class PhpCommand extends Command $options = array(); if ($this->isVerbose) { - $options[] = '--verbose'; + $options[] = '--verbose --testdox'; } if ($build) { $reportPath = $this->setupAndReturnReportDirectory(); @@ -71,7 +71,23 @@ class PhpCommand extends Command } chdir(realpath(__DIR__ . '/../..')); - passthru($phpUnit . ' ' . join(' ', array_merge($options, $this->params->getAllStandalone()))); + $command = $phpUnit . ' ' . join(' ', array_merge($options, $this->params->getAllStandalone())); + if ($this->isVerbose) { + $res = `$command`; + foreach (preg_split('/\n/', $res) as $line) { + if (preg_match('~\s+\[([x\s])\]\s~', $line, $m)) { + if ($m[1] === 'x') { + echo $this->screen->colorize($line, 'green') . "\n"; + } else { + echo $this->screen->colorize($line, 'red') . "\n"; + } + } else { + echo $line . "\n"; + } + } + } else { + passthru($command); + } } /** diff --git a/modules/test/phpunit.xml b/modules/test/phpunit.xml index 93907ec69..f2d8c0644 100644 --- a/modules/test/phpunit.xml +++ b/modules/test/phpunit.xml @@ -36,7 +36,6 @@ ../../application/forms/Config/LoggingForm.php ../../application/forms/Config/ConfirmRemovalForm.php ../../application/forms/Authentication - ../../application/forms/Install ../../application/forms/Dashboard ../../application/forms/Preference diff --git a/modules/translation/library/Translation/Util/GettextTranslationHelper.php b/modules/translation/library/Translation/Util/GettextTranslationHelper.php index 5702def85..0d5a950e1 100644 --- a/modules/translation/library/Translation/Util/GettextTranslationHelper.php +++ b/modules/translation/library/Translation/Util/GettextTranslationHelper.php @@ -101,7 +101,7 @@ class GettextTranslationHelper */ public function __construct(ApplicationBootstrap $bootstrap, $locale) { - $this->moduleMgr = $bootstrap->getModuleManager()->loadEnabledModules(); + $this->moduleMgr = $bootstrap->getModuleManager()->loadCoreModules()->loadEnabledModules(); $this->appDir = $bootstrap->getApplicationDir(); $this->locale = $locale; } diff --git a/packages/debian/control b/packages/debian/control index ab3162d46..48383839a 100644 --- a/packages/debian/control +++ b/packages/debian/control @@ -1,5 +1,5 @@ Source: icingaweb -Section: upstream +Section: admin Maintainer: Icinga Development Team Priority: optional Build-Depends: debhelper (>=9) @@ -28,7 +28,7 @@ Description: Icinga PHP common libraries Package: icingacli Architecture: any -Depends: libicinga-common-php (>= 2.0.0~alpha1) +Depends: libicinga-common-php (>= 2.0.0~alpha1), php5-cli (>= 5.3.2) Description: Icinga CLI tool The Icinga CLI allows one to access it's Icinga monitoring system from a terminal. diff --git a/packages/rpm/README.md b/packages/rpm/README.md index 5dd1006c5..293682a76 100644 --- a/packages/rpm/README.md +++ b/packages/rpm/README.md @@ -42,8 +42,7 @@ Decide whether to use MySQL or PostgreSQL. FLUSH PRIVILEGES; quit - mysql -u root -p icingaweb < /usr/share/doc/icingaweb2*/schema/accounts.mysql.sql - mysql -u root -p icingaweb < /usr/share/doc/icingaweb2*/schema/preferences.mysql.sql + mysql -u root -p icingaweb < /usr/share/doc/icingaweb2*/schema/mysql.sql ### PostgreSQL @@ -62,8 +61,7 @@ in `/var/lib/pgsql/data/pg_hba.conf` and restart the PostgreSQL server. Now install the `icingaweb` schema - bash$ psql -U icingaweb -a -f /usr/share/doc/icingaweb2*/schema/accounts.pgsql.sql - bash$ psql -U icingaweb -a -f /usr/share/doc/icingaweb2*/schema/preferences.pgsql.sql + bash$ psql -U icingaweb -a -f /usr/share/doc/icingaweb2*/schema/pgsql.sql ## Configuration diff --git a/public/css/icinga/defaults.less b/public/css/icinga/defaults.less index 34f1f110d..b657c9a86 100644 --- a/public/css/icinga/defaults.less +++ b/public/css/icinga/defaults.less @@ -51,6 +51,7 @@ a:hover { } #fontsize-calc { + display: none; width: 1000em; height: 1em; font-size: 1em; diff --git a/public/css/icinga/forms.less b/public/css/icinga/forms.less index b201df7c6..bf2f600c5 100644 --- a/public/css/icinga/forms.less +++ b/public/css/icinga/forms.less @@ -24,7 +24,6 @@ td.configTable { label { display: block; font-weight: bold; - margin-bottom: 0.3em; } input, select { @@ -81,6 +80,11 @@ input::-moz-focus-inner { outline: 0; } +button::-moz-focus-inner { + border: 0; + outline: 0; +} + select::-moz-focus-inner { border: 0; outline: 0; @@ -113,31 +117,63 @@ form.link-like input[type="submit"]:hover, form.link-like input[type="submit"]:f color: @colorLinkDefault; } -form ul.errors { +.non-list-like-list { list-style-type: none; margin: 0; padding: 0; + + li { + margin: 0.5em; + } } -form ul.errors li { - color: @colorCritical; - font-weight: bold; - line-height: 1.5em; +form div.element ul.errors { + .non-list-like-list; + margin: 0.3em 0 0 0.6em; + + li { + color: @colorCritical; + font-weight: bold; + line-height: 1.5em; + } } -#main form label { +form ul.form-errors { + .non-list-like-list; + display: inline-block; + margin-bottom: 1em; + border-radius: 1em; + background-color: @colorCritical; + + ul.errors { + .non-list-like-list; + } + + li { + color: white; + font-weight: bold; + line-height: 1.5em; + } +} + +form div.element { + margin: 0.5em 0; +} + +form label { display: inline-block; margin-right: 1em; font-size: 0.9em; width: 10em; } -#main select, #main input[type=text] { +select, input[type=text] { width: 20em; } -#main form .help-block, #main form .description { +form .description { font-size: 0.8em; + margin: 0.3em 0 0 0.6em; } .description { @@ -157,22 +193,3 @@ textarea { input, select, textarea { display: inline; } - -.control-group { - display: -webkit-flex; - display: flex; - -webkit-flex-flow: row wrap; - flex-flow: row wrap; -} - -.control-group > div { - width: 100%; - height: 100%; - overflow: auto; -} - -@media (min-width: 480px) { - .control-group > div { - width: auto; - } -} diff --git a/public/css/icinga/login.less b/public/css/icinga/login.less index 9f22fa60d..559ba1165 100644 --- a/public/css/icinga/login.less +++ b/public/css/icinga/login.less @@ -51,6 +51,14 @@ color: @colorTextDefault; } + .form div.element { + margin: 0; + + ul.errors { + margin-left: 9.5em; + } + } + .form label { font-weight: normal; display: inline-block; @@ -83,7 +91,7 @@ color: #eee; border-color: #777; background: #777; - margin-left: 11em; + margin-left: 9.9em; } input[type=submit]:hover, a.button:hover, input[type=submit]:focus { @@ -96,4 +104,13 @@ text-align: center; } + div.config-note { + width: 50%; + padding: 1em; + margin: 5em auto 0; + text-align: center; + border-radius: 0.5em; + border: 2px solid coral; + background-color: beige; + } } diff --git a/public/css/icinga/monitoring-colors.less b/public/css/icinga/monitoring-colors.less index c7a1c7054..78511918e 100644 --- a/public/css/icinga/monitoring-colors.less +++ b/public/css/icinga/monitoring-colors.less @@ -581,6 +581,15 @@ div.pivot-pagination { } } +table.joystick-pagination { + margin-top: -1.5em; + + td { + width: 1.25em; + height: 1.3em; + } +} + table.pivot { a { text-decoration: none; @@ -591,7 +600,7 @@ table.pivot { } } - thead { + & > thead { th { height: 6em; @@ -629,7 +638,7 @@ table.pivot { } } - tbody { + & > tbody { th { padding: 0 14px 0 0; white-space: nowrap; diff --git a/public/css/icinga/setup.less b/public/css/icinga/setup.less new file mode 100644 index 000000000..11f7bed97 --- /dev/null +++ b/public/css/icinga/setup.less @@ -0,0 +1,455 @@ +#layout { + overflow: auto; // TODO: Shouldn't be necessary, here, IMHO +} + +#setup { + .header { + width: 100%; + height: 5.4em; + background-color: #555; + background-image: -moz-linear-gradient(top, #777, #555); + background-image: -webkit-linear-gradient(top, #777, #555); + background-image: -o-linear-gradient(top, #777, #555); + background-image: -ms-linear-gradient(top, #777, #555); + background-image: linear-gradient(top, #777, #555); + + img { + width: 12.5em; + margin: 0.5em; + float: left; + } + + .progress-bar { + overflow: hidden; + padding-top: 1em; + + .step { + float: left; + + h1 { + margin: 0; + color: white; + font-size: 1em; + text-align: center; + } + + table { + margin-top: 0.7em; + + td { + padding: 0; + + &.left, &.right { + width: 50%; + } + } + } + + div { + background-color: lightgrey; + + &.line { + height: 0.5em; + + &.left { + margin-left: 0.1em; + margin-right: -0.1em; + border-top-left-radius: 0.5em; + border-bottom-left-radius: 0.5em; + } + + &.right { + margin-left: -0.1em; + margin-right: 0.1em; + border-top-right-radius: 0.5em; + border-bottom-right-radius: 0.5em; + } + } + + &.bubble { + width: 1.5em; + height: 1.5em; + border-radius: 1.5em; + + // Make sure that such a bubble overlays lines + position: relative; + z-index: 1337; + } + + &.active { + background-color: white; + } + + &.complete { + background-color: #2e8b57; + } + + &.visited { + background-color: #77bf97; + } + } + } + } + } + + .setup-content { + margin: 1.5em 1.8em 0 1.8em; + + form { + h2 { + font-size: 1.5em; + color: #444; + } + } + } +} + +#setup div.buttons { + margin: 1.5em 0; + + .double { + position: absolute; + left: -1337px; + } + + #btn_prev { + margin-right: 1em; + } + + button, .button-like { + padding: 0.5em 1em; + font-weight: bold; + outline: 0; + border: 1px solid black; + border-radius: 0.2em; + background: #666; + color: #eee; + + &[disabled="1"] { + background-color: #aaa; + border: 1px dotted black; + } + + &:hover, &:focus, &:active { + background-color: #333; + + &[disabled="1"] { + background-color: #aaa; + } + } + + &.finish, &.login { + min-width: 25em; + color: #fffafa; + background: #aaa; + + &:hover, &:focus, &:active { + background: #888; + } + } + } + + a.button-like { + cursor: default; + text-decoration: none; + } +} + +#setup table.requirements { + margin: -1em -1em 0; + border-spacing: 1em; + border-collapse: separate; + + td { + h2 { + margin: 0 1em 0 0; + } + + &.state { + color: white; + padding: 0.4em; + border-radius: 0.2em; + + &.fulfilled { + background-color: #4fad4b; + } + + &.not-available { + color: black; + background-color: #e8ec70; + } + + &.missing { + background-color: #fd7770; + } + } + + &.btn-update { + padding-top: 0.3em; + text-align: center; + + div.buttons { + margin: 0; + + a.button-like { + padding: 0.2em 0.5em; + background-color: #888; + + &:hover, &:focus { + background-color: #666; + } + } + } + } + } +} + +#setup_ldap_discovery_confirm table { + margin: 1em 0; + border-collapse: separate; + border-spacing: 1em 0.2em; +} + +#setup_admin_account { + div.instructions { + width: 30.2em; + display: inline-block; + } + + div.radiobox { + vertical-align: top; + display: inline-block; + padding: 0.9em 0.2em 0; + } +} + +#setup { + div.summary { + font-size: 90%; + + div.page { + float: left; + width: 25em; + min-height: 25em; + padding: 0 1em 1em; + margin: 1em 1.5em 1.5em; + border-radius: 0.5em; + border: 1px dashed lightgrey; + + h2 { + font-size: 1.2em; + } + + div.topic { + margin-left: 2em; + + h3 { + font-size: 1em; + } + + ul { + list-style-type: circle; + } + + table { + border-spacing: 0.5em; + border-collapse: separate; + font-size: 0.9em; + margin-left: 2em; + } + } + } + } + + form#setup_summary { + clear: left; + } +} + +.conspicuous-state-notification { + width: 66%; + margin: 0 auto; + padding: 0.5em; + color: white; + font-weight: bold; + border-radius: 1em; +} + +#setup-finish { + div.report { + padding: 1em; + border-radius: 2em; + background-color: #eee; + + div.line-separator { + width: 50%; + height: 1px; + margin: 0 auto; + background-color: white; + } + + p { + margin: 1em; + color: #444; + text-align: center; + + &.error { + color: red; + } + + &.failure { + .conspicuous-state-notification; + background-color: @colorCritical; + } + + &.success { + .conspicuous-state-notification; + background-color: @colorOk; + } + } + } + + div.buttons { + text-align: center; + } +} + +.welcome-page { + margin-top: 3em; + text-align: center; + + h2 { + font-size: 1.5em; + } + + div.info { + display: inline-block; + max-width: 66%; + padding: 0 1em; + border-radius: 1em; + background-color: #eee; + border: 1px solid lightgrey; + } + + p.restart-warning { + color: coral; + font-weight: bold; + } + + div.note { + width: 40%; + padding: 1em 1em 0; + margin: 3em auto 0; + text-align: left; + border-radius: 0.5em; + border: 1px solid #eee; + + div.title { + padding: 0.2em; + margin: -1em -1em 1em; + text-align: center; + background-color: #eee; + border-top-left-radius: 0.5em; + border-top-right-radius: 0.5em; + } + + img { + float: right; + } + + p { + margin: 2em 0 1em 0; + + &:first-child { + margin-top: 1em; + } + } + + div.code { + margin: 0 2em; + + span { + display: block; + font-family: monospace; + } + } + } +} + +#setup_monitoring_welcome { + .welcome-page; + margin-top: 0; + padding: 1em; + + h2 { + margin-top: 0; + } +} + +#setup { + div.module-wizard { + width: auto; + padding: 1em; + overflow: hidden; + border-radius: 1em; + background-color: #f4f4f4; + + div.buttons { + margin: 1.5em 0 0; + + button[type=submit] { + padding: 0.5em; + line-height: 0.5em; + background-color: #888; + + &:hover, &:focus, &:active { + background-color: #666; + } + } + } + } + + div.module-menu { + width: 25%; + float: right; + margin-left: 1.5em; + + p { + margin-top: 0; + + &.all-completed { + .conspicuous-state-notification; + text-align: center; + font-size: 90%; + background-color: @colorOk; + } + } + + ul { + padding-left: 1.2em; + + button { + margin: 0 0 0.8em; + padding: 0; + color: black; + border: none; + outline: none; + font-size: 90%; + background-color: white; + + &:hover { + color: #666; + cursor: pointer; + } + + &:focus, &:active { + color: #666; + } + } + + img { + margin: 0 0.5em 0.2em; + } + } + } +} diff --git a/public/index.php b/public/index.php index 171581155..48a500666 100644 --- a/public/index.php +++ b/public/index.php @@ -2,4 +2,4 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -require_once dirname(__DIR__). '/library/Icinga/Application/webrouter.php'; +require_once dirname(__DIR__) . '/library/Icinga/Application/webrouter.php'; diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index 5f62a8c8a..e2d5f03bd 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -197,6 +197,13 @@ var $target; var data; + if ($button.length === 0) { + var $el = $(event.currentTarget); + if ($el.is('input[type=submit]') || $el.is('button[type=submit]')) { + $button = $el; + } + } + if (typeof method === 'undefined') { method = 'POST'; } else { @@ -204,7 +211,7 @@ } if ($button.length === 0) { - $button = $('input[type=submit]', $form).first(); + $button = $('input[type=submit]', $form).add('button[type=submit]', $form).first(); } event.stopPropagation(); diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index dca88c394..31a0d10a8 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -269,15 +269,24 @@ } } else { - if (req.$target.attr('id') === 'col2') { // TODO: multicol - if ($('#col1').data('icingaUrl') === redirect) { - icinga.ui.layout1col(); - req.$target = $('#col1'); - delete(this.requests['col2']); - } - } - this.loadUrl(redirect, req.$target); + if (redirect.match(/#!/)) { + var parts = redirect.split(/#!/); + icinga.ui.layout2col(); + this.loadUrl(parts.shift(), $('#col1')); + this.loadUrl(parts.shift(), $('#col2')); + } else { + + if (req.$target.attr('id') === 'col2') { // TODO: multicol + if ($('#col1').data('icingaUrl') === redirect) { + icinga.ui.layout1col(); + req.$target = $('#col1'); + delete(this.requests['col2']); + } + } + + this.loadUrl(redirect, req.$target); + } } return true; }, @@ -672,6 +681,8 @@ } } + $container.trigger('beforerender'); + var discard = false; $.each(self.icinga.behaviors, function(name, behavior) { if (behavior.renderHook) { @@ -691,11 +702,12 @@ var $content = $('
' + content + '
'); // Disable all click events while rendering - $('*').click(function (event) { - event.stopImmediatePropagation(); - event.stopPropagation(); - event.preventDefault(); - }); + // (Disabling disabled, was ways too slow) + // $('*').click(function (event) { + // event.stopImmediatePropagation(); + // event.stopPropagation(); + // event.preventDefault(); + // }); $('.container', $container).each(function() { self.stopPendingRequestsFor($(this)); @@ -738,8 +750,8 @@ icinga.ui.initializeControls($container); icinga.ui.fixControls(); - // Re-enable all click events - $('*').off('click'); + // Re-enable all click events (disabled as of performance reasons) + // $('*').off('click'); }, /** diff --git a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php b/test/php/application/forms/Config/Authentication/DbBackendFormTest.php index 3364dff60..60d5a0506 100644 --- a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php +++ b/test/php/application/forms/Config/Authentication/DbBackendFormTest.php @@ -9,6 +9,7 @@ namespace Tests\Icinga\Form\Config\Authentication; require_once realpath(dirname(__FILE__) . '/../../../../bootstrap.php'); use Mockery; +use Icinga\Application\Config; use Icinga\Test\BaseTestCase; use Icinga\Form\Config\Authentication\DbBackendForm; @@ -37,7 +38,7 @@ class DbBackendFormTest extends BaseTestCase $form->populate(array('resource' => 'test_db_backend')); $this->assertTrue( - $form->isValidAuthenticationBackend($form), + DbBackendForm::isValidAuthenticationBackend($form), 'DbBackendForm claims that a valid authentication backend with users is not valid' ); } @@ -59,7 +60,7 @@ class DbBackendFormTest extends BaseTestCase $form->populate(array('resource' => 'test_db_backend')); $this->assertFalse( - $form->isValidAuthenticationBackend($form), + DbBackendForm::isValidAuthenticationBackend($form), 'DbBackendForm claims that an invalid authentication backend without users is valid' ); } @@ -67,7 +68,9 @@ class DbBackendFormTest extends BaseTestCase protected function setUpResourceFactoryMock() { Mockery::mock('alias:Icinga\Data\ResourceFactory') - ->shouldReceive('create') - ->andReturn(Mockery::mock('Icinga\Data\Db\DbConnection')); + ->shouldReceive('createResource') + ->andReturn(Mockery::mock('Icinga\Data\Db\DbConnection')) + ->shouldReceive('getResourceConfig') + ->andReturn(new Config()); } } diff --git a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php b/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php index 0335d1f82..ac7834489 100644 --- a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php +++ b/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php @@ -10,6 +10,7 @@ require_once realpath(dirname(__FILE__) . '/../../../../bootstrap.php'); use Mockery; use Icinga\Test\BaseTestCase; +use Icinga\Application\Config; use Icinga\Form\Config\Authentication\LdapBackendForm; use Icinga\Exception\AuthenticationException; @@ -37,7 +38,7 @@ class LdapBackendFormTest extends BaseTestCase $form->populate(array('resource' => 'test_ldap_backend')); $this->assertTrue( - $form->isValidAuthenticationBackend($form), + LdapBackendForm::isValidAuthenticationBackend($form), 'LdapBackendForm claims that a valid authentication backend with users is not valid' ); } @@ -58,7 +59,7 @@ class LdapBackendFormTest extends BaseTestCase $form->populate(array('resource' => 'test_ldap_backend')); $this->assertFalse( - $form->isValidAuthenticationBackend($form), + LdapBackendForm::isValidAuthenticationBackend($form), 'LdapBackendForm claims that an invalid authentication backend without users is valid' ); } @@ -66,9 +67,9 @@ class LdapBackendFormTest extends BaseTestCase protected function setUpResourceFactoryMock() { Mockery::mock('alias:Icinga\Data\ResourceFactory') + ->shouldReceive('createResource') + ->andReturn(Mockery::mock('Icinga\Protocol\Ldap\Connection')) ->shouldReceive('getResourceConfig') - ->andReturn(new \Zend_Config(array())) - ->shouldReceive('create') - ->andReturn(Mockery::mock('Icinga\Protocol\Ldap\Connection')); + ->andReturn(new Config()); } } diff --git a/test/php/application/forms/Config/Resource/DbResourceFormTest.php b/test/php/application/forms/Config/Resource/DbResourceFormTest.php index 1b624c49f..1ee39a4fa 100644 --- a/test/php/application/forms/Config/Resource/DbResourceFormTest.php +++ b/test/php/application/forms/Config/Resource/DbResourceFormTest.php @@ -30,10 +30,8 @@ class DbResourceFormTest extends BaseTestCase Mockery::mock()->shouldReceive('getConnection')->atMost()->twice()->andReturn(Mockery::self())->getMock() ); - $form = new DbResourceForm(); - $this->assertTrue( - $form->isValidResource($form), + DbResourceForm::isValidResource(new DbResourceForm()), 'ResourceForm claims that a valid db resource is not valid' ); } @@ -48,10 +46,8 @@ class DbResourceFormTest extends BaseTestCase Mockery::mock()->shouldReceive('getConnection')->once()->andThrow('\Exception')->getMock() ); - $form = new DbResourceForm(); - $this->assertFalse( - $form->isValidResource($form), + DbResourceForm::isValidResource(new DbResourceForm()), 'ResourceForm claims that an invalid db resource is valid' ); } @@ -60,7 +56,7 @@ class DbResourceFormTest extends BaseTestCase { Mockery::mock('alias:Icinga\Data\ResourceFactory') ->shouldReceive('createResource') - ->with(Mockery::type('\Zend_Config')) + ->with(Mockery::type('Icinga\Application\Config')) ->andReturn($resourceMock); } } diff --git a/test/php/application/forms/Config/Resource/LdapResourceFormTest.php b/test/php/application/forms/Config/Resource/LdapResourceFormTest.php index 078a11146..66f274b30 100644 --- a/test/php/application/forms/Config/Resource/LdapResourceFormTest.php +++ b/test/php/application/forms/Config/Resource/LdapResourceFormTest.php @@ -27,13 +27,14 @@ class LdapResourceFormTest extends BaseTestCase public function testValidLdapResourceIsValid() { $this->setUpResourceFactoryMock( - Mockery::mock()->shouldReceive('connect')->getMock() + Mockery::mock()->shouldReceive('testCredentials')->once()->andReturn(true)->getMock() ); $form = new LdapResourceForm(); + $form->setTokenDisabled(); $this->assertTrue( - $form->isValidResource($form), + LdapResourceForm::isValidResource($form->create()), 'ResourceForm claims that a valid ldap resource is not valid' ); } @@ -45,13 +46,14 @@ class LdapResourceFormTest extends BaseTestCase public function testInvalidLdapResourceIsNotValid() { $this->setUpResourceFactoryMock( - Mockery::mock()->shouldReceive('connect')->once()->andThrow('\Exception')->getMock() + Mockery::mock()->shouldReceive('testCredentials')->once()->andThrow('\Exception')->getMock() ); $form = new LdapResourceForm(); + $form->setTokenDisabled(); $this->assertFalse( - $form->isValidResource($form), + LdapResourceForm::isValidResource($form->create()), 'ResourceForm claims that an invalid ldap resource is valid' ); } @@ -60,7 +62,7 @@ class LdapResourceFormTest extends BaseTestCase { Mockery::mock('alias:Icinga\Data\ResourceFactory') ->shouldReceive('createResource') - ->with(Mockery::type('\Zend_Config')) + ->with(Mockery::type('Icinga\Application\Config')) ->andReturn($resourceMock); } } diff --git a/test/php/application/forms/Config/Resource/LivestatusResourceFormTest.php b/test/php/application/forms/Config/Resource/LivestatusResourceFormTest.php index 300adebed..556a0fd27 100644 --- a/test/php/application/forms/Config/Resource/LivestatusResourceFormTest.php +++ b/test/php/application/forms/Config/Resource/LivestatusResourceFormTest.php @@ -31,10 +31,8 @@ class LivestatusResourceFormTest extends BaseTestCase ->shouldReceive('disconnect')->getMock() ); - $form = new LivestatusResourceForm(); - $this->assertTrue( - $form->isValidResource($form), + LivestatusResourceForm::isValidResource(new LivestatusResourceForm()), 'ResourceForm claims that a valid livestatus resource is not valid' ); } @@ -49,10 +47,8 @@ class LivestatusResourceFormTest extends BaseTestCase Mockery::mock()->shouldReceive('connect')->once()->andThrow('\Exception')->getMock() ); - $form = new LivestatusResourceForm(); - $this->assertFalse( - $form->isValidResource($form), + LivestatusResourceForm::isValidResource(new LivestatusResourceForm()), 'ResourceForm claims that an invalid livestatus resource is valid' ); } @@ -61,7 +57,7 @@ class LivestatusResourceFormTest extends BaseTestCase { Mockery::mock('alias:Icinga\Data\ResourceFactory') ->shouldReceive('createResource') - ->with(Mockery::type('\Zend_Config')) + ->with(Mockery::type('Icinga\Application\Config')) ->andReturn($resourceMock); } } diff --git a/test/php/library/Icinga/Application/ConfigTest.php b/test/php/library/Icinga/Application/ConfigTest.php index e99b17b2e..4f4f77219 100644 --- a/test/php/library/Icinga/Application/ConfigTest.php +++ b/test/php/library/Icinga/Application/ConfigTest.php @@ -5,7 +5,7 @@ namespace Tests\Icinga\Application; use Icinga\Test\BaseTestCase; -use Icinga\Application\Config as IcingaConfig; +use Icinga\Application\Config; class ConfigTest extends BaseTestCase { @@ -15,8 +15,8 @@ class ConfigTest extends BaseTestCase public function setUp() { parent::setUp(); - $this->configDir = IcingaConfig::$configDir; - IcingaConfig::$configDir = dirname(__FILE__) . '/ConfigTest/files'; + $this->oldConfigDir = Config::$configDir; + Config::$configDir = dirname(__FILE__) . '/ConfigTest/files'; } /** @@ -25,77 +25,351 @@ class ConfigTest extends BaseTestCase public function tearDown() { parent::tearDown(); - IcingaConfig::$configDir = $this->configDir; + Config::$configDir = $this->oldConfigDir; } - public function testAppConfig() + public function testWhetherInitializingAConfigWithAssociativeArraysCreatesHierarchicalConfigObjects() { - $config = IcingaConfig::app('config', true); - $this->assertEquals(1, $config->logging->enable, 'Unexpected value retrieved from config file'); - // Test non-existent property where null is the default value - $this->assertEquals( - null, - $config->logging->get('disable'), - 'Unexpected default value for non-existent properties' + $config = new Config(array( + 'a' => 'b', + 'c' => 'd', + 'e' => array( + 'f' => 'g', + 'h' => 'i', + 'j' => array( + 'k' => 'l', + 'm' => 'n' + ) + ) + )); + + $this->assertInstanceOf( + get_class($config), + $config->e, + 'Config::__construct() does not accept two dimensional arrays' ); - // Test non-existent property using zero as the default value - $this->assertEquals(0, $config->logging->get('disable', 0)); - // Test retrieve full section + $this->assertInstanceOf( + get_class($config), + $config->e->j, + 'Config::__construct() does not accept multi dimensional arrays' + ); + } + + /** + * @depends testWhetherInitializingAConfigWithAssociativeArraysCreatesHierarchicalConfigObjects + */ + public function testWhetherItIsPossibleToCloneConfigObjects() + { + $config = new Config(array( + 'a' => 'b', + 'c' => array( + 'd' => 'e' + ) + )); + $newConfig = clone $config; + + $this->assertNotSame( + $config, + $newConfig, + 'Shallow cloning objects of type Config does not seem to work properly' + ); + $this->assertNotSame( + $config->c, + $newConfig->c, + 'Deep cloning objects of type Config does not seem to work properly' + ); + } + + public function testWhetherConfigObjectsAreCountable() + { + $config = new Config(array('a' => 'b', 'c' => array('d' => 'e'))); + + $this->assertInstanceOf('Countable', $config, 'Config objects do not implement interface `Countable\''); + $this->assertEquals(2, count($config), 'Config objects do not count properties and sections correctly'); + } + + public function testWhetherConfigObjectsAreTraversable() + { + $config = new Config(array('a' => 'b', 'c' => 'd')); + $config->e = 'f'; + + $this->assertInstanceOf('Iterator', $config, 'Config objects do not implement interface `Iterator\''); + + $actual = array(); + foreach ($config as $key => $value) { + $actual[$key] = $value; + } + + $this->assertEquals( + array('a' => 'b', 'c' => 'd', 'e' => 'f'), + $actual, + 'Config objects do not iterate properly in the order their values were inserted' + ); + } + + public function testWhetherOneCanCheckWhetherConfigObjectsHaveACertainPropertyOrSection() + { + $config = new Config(array('a' => 'b', 'c' => array('d' => 'e'))); + + $this->assertTrue(isset($config->a), 'Config objects do not seem to implement __isset() properly'); + $this->assertTrue(isset($config->c->d), 'Config objects do not seem to implement __isset() properly'); + $this->assertFalse(isset($config->d), 'Config objects do not seem to implement __isset() properly'); + $this->assertFalse(isset($config->c->e), 'Config objects do not seem to implement __isset() properly'); + $this->assertTrue(isset($config['a']), 'Config object do not seem to implement offsetExists() properly'); + $this->assertFalse(isset($config['d']), 'Config object do not seem to implement offsetExists() properly'); + } + + public function testWhetherItIsPossibleToAccessProperties() + { + $config = new Config(array('a' => 'b', 'c' => null)); + + $this->assertEquals('b', $config->a, 'Config objects do not allow property access'); + $this->assertNull($config['c'], 'Config objects do not allow offset access'); + $this->assertNull($config->d, 'Config objects do not return NULL as default'); + } + + public function testWhetherItIsPossibleToSetPropertiesAndSections() + { + $config = new Config(); + $config->a = 'b'; + $config['c'] = array('d' => 'e'); + + $this->assertTrue(isset($config->a), 'Config objects do not allow to set properties'); + $this->assertTrue(isset($config->c), 'Config objects do not allow to set offsets'); + $this->assertInstanceOf( + get_class($config), + $config->c, + 'Config objects do not convert arrays to config objects when set' + ); + } + + /** + * @expectedException LogicException + */ + public function testWhetherItIsNotPossibleToAppendProperties() + { + $config = new Config(); + $config[] = 'test'; + } + + public function testWhetherItIsPossibleToUnsetPropertiesAndSections() + { + $config = new Config(array('a' => 'b', 'c' => array('d' => 'e'))); + unset($config->a); + unset($config['c']); + + $this->assertFalse(isset($config->a), 'Config objects do not allow to unset properties'); + $this->assertFalse(isset($config->c), 'Config objects do not allow to unset sections'); + } + + /** + * @depends testWhetherConfigObjectsAreCountable + */ + public function testWhetherOneCanCheckIfAConfigObjectHasAnyPropertiesOrSections() + { + $config = new Config(); + $this->assertTrue($config->isEmpty(), 'Config objects do not report that they are empty'); + + $config->test = 'test'; + $this->assertFalse($config->isEmpty(), 'Config objects do report that they are empty although they are not'); + } + + /** + * @depends testWhetherItIsPossibleToAccessProperties + */ + public function testWhetherItIsPossibleToRetrieveDefaultValuesForNonExistentPropertiesOrSections() + { + $config = new Config(array('a' => 'b')); + + $this->assertEquals( + 'b', + $config->get('a'), + 'Config objects do not return the actual value of existing properties' + ); + $this->assertNull( + $config->get('b'), + 'Config objects do not return NULL as default for non-existent properties' + ); + $this->assertEquals( + 'test', + $config->get('test', 'test'), + 'Config objects do not allow to define the default value to return for non-existent properties' + ); + } + + public function testWhetherItIsPossibleToRetrieveAllPropertyAndSectionNames() + { + $config = new Config(array('a' => 'b', 'c' => array('d' => 'e'))); + + $this->assertEquals( + array('a', 'c'), + $config->keys(), + 'Config objects do not list property and section names correctly' + ); + } + + public function testWhetherConfigObjectsCanBeConvertedToArrays() + { + $config = new Config(array('a' => 'b', 'c' => array('d' => 'e'))); + + $this->assertEquals( + array('a' => 'b', 'c' => array('d' => 'e')), + $config->toArray(), + 'Config objects cannot be correctly converted to arrays' + ); + } + + /** + * @depends testWhetherConfigObjectsCanBeConvertedToArrays + */ + public function testWhetherItIsPossibleToMergeConfigObjects() + { + $config = new Config(array('a' => 'b')); + + $config->merge(array('a' => 'bb', 'c' => 'd', 'e' => array('f' => 'g'))); + $this->assertEquals( + array('a' => 'bb', 'c' => 'd', 'e' => array('f' => 'g')), + $config->toArray(), + 'Config objects cannot be extended with arrays' + ); + + $config->merge(new Config(array('c' => array('d' => 'ee'), 'e' => array('h' => 'i')))); + $this->assertEquals( + array('a' => 'bb', 'c' => array('d' => 'ee'), 'e' => array('f' => 'g', 'h' => 'i')), + $config->toArray(), + 'Config objects cannot be extended with other Config objects' + ); + } + + /** + * @depends testWhetherItIsPossibleToAccessProperties + */ + public function testWhetherItIsPossibleToDirectlyRetrieveASectionProperty() + { + $config = new Config(array('a' => array('b' => 'c'))); + + $this->assertEquals( + 'c', + $config->fromSection('a', 'b'), + 'Config::fromSection does not return the actual value of a section\'s property' + ); + $this->assertNull( + $config->fromSection('a', 'c'), + 'Config::fromSection does not return NULL as default for non-existent section properties' + ); + $this->assertNull( + $config->fromSection('b', 'c'), + 'Config::fromSection does not return NULL as default for non-existent sections' + ); + $this->assertEquals( + 'test', + $config->fromSection('a', 'c', 'test'), + 'Config::fromSection does not return the given default value for non-existent section properties' + ); + $this->assertEquals( + 'c', + $config->fromSection('a', 'b', 'test'), + 'Config::fromSection does not return the actual value of a section\'s property in case a default is given' + ); + } + + /** + * @expectedException UnexpectedValueException + * @depends testWhetherItIsPossibleToAccessProperties + */ + public function testWhetherAnExceptionIsThrownWhenTryingToAccessASectionPropertyOnANonSection() + { + $config = new Config(array('a' => 'b')); + $config->fromSection('a', 'b'); + } + + public function testWhetherConfigResolvePathReturnsValidAbsolutePaths() + { + $this->assertEquals( + Config::$configDir . DIRECTORY_SEPARATOR . 'a' . DIRECTORY_SEPARATOR . 'b.ini', + Config::resolvePath(DIRECTORY_SEPARATOR . 'a' . DIRECTORY_SEPARATOR . 'b.ini'), + 'Config::resolvePath does not produce valid absolute paths' + ); + } + + /** + * @depends testWhetherConfigObjectsCanBeConvertedToArrays + * @depends testWhetherConfigResolvePathReturnsValidAbsolutePaths + */ + public function testWhetherItIsPossibleToInitializeAConfigObjectFromAIniFile() + { + $config = Config::fromIni(Config::resolvePath('config.ini')); + $this->assertEquals( array( - 'disable' => 1, - 'db' => array( - 'user' => 'user', - 'password' => 'password' + 'logging' => array( + 'enable' => 1 + ), + 'backend' => array( + 'type' => 'db', + 'user' => 'user', + 'password' => 'password', + 'disable' => 1 ) ), - $config->backend->toArray() + $config->toArray(), + 'Config::fromIni does not load INI files correctly' ); - // Test non-existent section using 'default' as default value - $this->assertEquals('default', $config->get('magic', 'default')); - // Test sub-properties - $this->assertEquals('user', $config->backend->db->user); - // Test non-existent sub-property using 'UTF-8' as the default value - $this->assertEquals('UTF-8', $config->backend->db->get('encoding', 'UTF-8')); - // Test invalid property names using false as default value - $this->assertEquals(false, $config->backend->get('.', false)); - $this->assertEquals(false, $config->backend->get('db.', false)); - $this->assertEquals(false, $config->backend->get('.user', false)); - // Test retrieve array of sub-properties + + $this->assertInstanceOf( + get_class($config), + Config::fromIni('nichda'), + 'Config::fromIni does not return empty configs for non-existent configuration files' + ); + } + + /** + * @expectedException Icinga\Exception\NotReadableError + */ + public function testWhetherFromIniThrowsAnExceptionOnInsufficientPermission() + { + Config::fromIni('/etc/shadow'); + } + + /** + * @depends testWhetherItIsPossibleToInitializeAConfigObjectFromAIniFile + */ + public function testWhetherItIsPossibleToRetrieveApplicationConfiguration() + { + $config = Config::app(); + $this->assertEquals( array( - 'user' => 'user', - 'password' => 'password' + 'logging' => array( + 'enable' => 1 + ), + 'backend' => array( + 'type' => 'db', + 'user' => 'user', + 'password' => 'password', + 'disable' => 1 + ) ), - $config->backend->db->toArray() + $config->toArray(), + 'Config::app does not load INI files correctly' ); - // Test singleton - $this->assertEquals($config, IcingaConfig::app('config')); - $this->assertEquals(array('logging', 'backend'), $config->keys()); - $this->assertEquals(array('enable'), $config->keys('logging')); } - public function testAppExtraConfig() + /** + * @depends testWhetherItIsPossibleToInitializeAConfigObjectFromAIniFile + */ + public function testWhetherItIsPossibleToRetrieveModuleConfiguration() { - $extraConfig = IcingaConfig::app('extra', true); - $this->assertEquals(1, $extraConfig->meta->version); - $this->assertEquals($extraConfig, IcingaConfig::app('extra')); - } + $config = Config::module('amodule'); - public function testModuleConfig() - { - $moduleConfig = IcingaConfig::module('amodule', 'config', true); - $this->assertEquals(1, $moduleConfig->menu->get('breadcrumb')); - $this->assertEquals($moduleConfig, IcingaConfig::module('amodule')); - } - - public function testModuleExtraConfig() - { - $moduleExtraConfig = IcingaConfig::module('amodule', 'extra', true); $this->assertEquals( - 'inetOrgPerson', - $moduleExtraConfig->ldap->user->get('ldap_object_class') + array( + 'menu' => array( + 'breadcrumb' => 1 + ) + ), + $config->toArray(), + 'Config::module does not load INI files correctly' ); - $this->assertEquals($moduleExtraConfig, IcingaConfig::module('amodule', 'extra')); } } diff --git a/test/php/library/Icinga/Application/ConfigTest/files/config.ini b/test/php/library/Icinga/Application/ConfigTest/files/config.ini index b74ea65c5..aa729ae58 100644 --- a/test/php/library/Icinga/Application/ConfigTest/files/config.ini +++ b/test/php/library/Icinga/Application/ConfigTest/files/config.ini @@ -2,6 +2,7 @@ enable = 1 [backend] -db.user = 'user' -db.password = 'password' +type = "db" +user = "user" +password = "password" disable = 1 diff --git a/test/php/library/Icinga/Application/ConfigTest/files/extra.ini b/test/php/library/Icinga/Application/ConfigTest/files/extra.ini deleted file mode 100644 index fc203052e..000000000 --- a/test/php/library/Icinga/Application/ConfigTest/files/extra.ini +++ /dev/null @@ -1,2 +0,0 @@ -[meta] -version = 1 diff --git a/test/php/library/Icinga/Application/ConfigTest/files/modules/amodule/extra.ini b/test/php/library/Icinga/Application/ConfigTest/files/modules/amodule/extra.ini deleted file mode 100644 index 38fd04b27..000000000 --- a/test/php/library/Icinga/Application/ConfigTest/files/modules/amodule/extra.ini +++ /dev/null @@ -1,2 +0,0 @@ -[ldap] -user.ldap_object_class = inetOrgPerson diff --git a/test/php/library/Icinga/Config/PreservingIniWriterTest.php b/test/php/library/Icinga/File/Ini/IniWriterTest.php similarity index 60% rename from test/php/library/Icinga/Config/PreservingIniWriterTest.php rename to test/php/library/Icinga/File/Ini/IniWriterTest.php index da7e9674d..1b9580fb3 100644 --- a/test/php/library/Icinga/Config/PreservingIniWriterTest.php +++ b/test/php/library/Icinga/File/Ini/IniWriterTest.php @@ -4,12 +4,11 @@ namespace Tests\Icinga\Config; -use Zend_Config; -use Zend_Config_Ini; +use Icinga\File\Ini\IniWriter; use Icinga\Test\BaseTestCase; -use Icinga\Config\PreservingIniWriter; +use Icinga\Application\Config; -class PreservingIniWriterTest extends BaseTestCase +class IniWriterTest extends BaseTestCase { protected $tempFile; protected $tempFile2; @@ -30,26 +29,52 @@ class PreservingIniWriterTest extends BaseTestCase unlink($this->tempFile2); } + public function testWhetherPointInSectionIsNotNormalized() + { + $writer = new IniWriter( + array( + 'config' => new Config( + array( + 'section' => array( + 'foo.bar' => 1337 + ), + 'section.with.multiple.dots' => array( + 'some more' => array( + 'nested stuff' => 'With more values' + ) + ) + ) + ), + 'filename' => $this->tempFile + ) + ); + $writer->write(); + $config = Config::fromIni($this->tempFile)->toArray(); + $this->assertTrue(array_key_exists('section.with.multiple.dots', $config), 'Section names not normalized'); + } + public function testWhetherSimplePropertiesAreInsertedInEmptyFiles() { + $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); $target = $this->writeConfigToTemporaryFile(''); - $config = new Zend_Config(array('key' => 'value')); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array('key' => 'value')); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); - $this->assertEquals('value', $newConfig->get('key'), 'PreservingIniWriter does not insert in empty files'); + $newConfig = Config::fromIni($target); + $this->assertEquals('value', $newConfig->get('key'), 'IniWriter does not insert in empty files'); } public function testWhetherSimplePropertiesAreInsertedInExistingFiles() { + $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); $target = $this->writeConfigToTemporaryFile('key1 = "1"'); - $config = new Zend_Config(array('key2' => '2')); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array('key2' => '2')); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); - $this->assertEquals('2', $newConfig->get('key2'), 'PreservingIniWriter does not insert in existing files'); + $newConfig = Config::fromIni($target); + $this->assertEquals('2', $newConfig->get('key2'), 'IniWriter does not insert in existing files'); } /** @@ -57,13 +82,14 @@ class PreservingIniWriterTest extends BaseTestCase */ public function testWhetherSimplePropertiesAreUpdated() { + $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); $target = $this->writeConfigToTemporaryFile('key = "value"'); - $config = new Zend_Config(array('key' => 'eulav')); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array('key' => 'eulav')); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); - $this->assertEquals('eulav', $newConfig->get('key'), 'PreservingIniWriter does not update simple properties'); + $newConfig = Config::fromIni($target); + $this->assertEquals('eulav', $newConfig->get('key'), 'IniWriter does not update simple properties'); } /** @@ -71,32 +97,33 @@ class PreservingIniWriterTest extends BaseTestCase */ public function testWhetherSimplePropertiesAreDeleted() { + $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); $target = $this->writeConfigToTemporaryFile('key = "value"'); - $config = new Zend_Config(array()); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array()); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); - $this->assertNull($newConfig->get('key'), 'PreservingIniWriter does not delete simple properties'); + $newConfig = Config::fromIni($target); + $this->assertNull($newConfig->get('key'), 'IniWriter does not delete simple properties'); } public function testWhetherNestedPropertiesAreInserted() { $target = $this->writeConfigToTemporaryFile(''); - $config = new Zend_Config(array('a' => array('b' => 'c'))); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array('a' => array('b' => 'c'))); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertInstanceOf( - '\Zend_Config', + get_class($newConfig), $newConfig->get('a'), - 'PreservingIniWriter does not insert nested properties' + 'IniWriter does not insert nested properties' ); $this->assertEquals( 'c', $newConfig->get('a')->get('b'), - 'PreservingIniWriter does not insert nested properties' + 'IniWriter does not insert nested properties' ); } @@ -105,21 +132,22 @@ class PreservingIniWriterTest extends BaseTestCase */ public function testWhetherNestedPropertiesAreUpdated() { + $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); $target = $this->writeConfigToTemporaryFile('a.b = "c"'); - $config = new Zend_Config(array('a' => array('b' => 'cc'))); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array('a' => array('b' => 'cc'))); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertInstanceOf( - '\Zend_Config', + get_class($newConfig), $newConfig->get('a'), - 'PreservingIniWriter does not update nested properties' + 'IniWriter does not update nested properties' ); $this->assertEquals( 'cc', $newConfig->get('a')->get('b'), - 'PreservingIniWriter does not update nested properties' + 'IniWriter does not update nested properties' ); } @@ -128,35 +156,36 @@ class PreservingIniWriterTest extends BaseTestCase */ public function testWhetherNestedPropertiesAreDeleted() { + $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); $target = $this->writeConfigToTemporaryFile('a.b = "c"'); - $config = new Zend_Config(array()); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array()); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertNull( $newConfig->get('a'), - 'PreservingIniWriter does not delete nested properties' + 'IniWriter does not delete nested properties' ); } public function testWhetherSimpleSectionPropertiesAreInserted() { $target = $this->writeConfigToTemporaryFile(''); - $config = new Zend_Config(array('section' => array('key' => 'value'))); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array('section' => array('key' => 'value'))); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertInstanceOf( - '\Zend_Config', + get_class($newConfig), $newConfig->get('section'), - 'PreservingIniWriter does not insert sections' + 'IniWriter does not insert sections' ); $this->assertEquals( 'value', $newConfig->get('section')->get('key'), - 'PreservingIniWriter does not insert simple section properties' + 'IniWriter does not insert simple section properties' ); } @@ -170,15 +199,15 @@ class PreservingIniWriterTest extends BaseTestCase key = "value" EOD ); - $config = new Zend_Config(array('section' => array('key' => 'eulav'))); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array('section' => array('key' => 'eulav'))); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertEquals( 'eulav', $newConfig->get('section')->get('key'), - 'PreservingIniWriter does not update simple section properties' + 'IniWriter does not update simple section properties' ); } @@ -192,39 +221,40 @@ EOD key = "value" EOD ); - $config = new Zend_Config(array('section' => array())); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array('section' => array())); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertNull( $newConfig->get('section')->get('key'), - 'PreservingIniWriter does not delete simple section properties' + 'IniWriter does not delete simple section properties' ); } public function testWhetherNestedSectionPropertiesAreInserted() { + $this->markTestSkipped('Implementation has changed. Config::fromIni cannot handle nested properties anymore'); $target = $this->writeConfigToTemporaryFile(''); - $config = new Zend_Config(array('section' => array('a' => array('b' => 'c')))); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array('section' => array('a' => array('b' => 'c')))); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertInstanceOf( - '\Zend_Config', + get_class($newConfig), $newConfig->get('section'), - 'PreservingIniWriter does not insert sections' + 'IniWriter does not insert sections' ); $this->assertInstanceOf( - '\Zend_Config', + get_class($newConfig), $newConfig->get('section')->get('a'), - 'PreservingIniWriter does not insert nested section properties' + 'IniWriter does not insert nested section properties' ); $this->assertEquals( 'c', $newConfig->get('section')->get('a')->get('b'), - 'PreservingIniWriter does not insert nested section properties' + 'IniWriter does not insert nested section properties' ); } @@ -238,15 +268,15 @@ EOD a.b = "c" EOD ); - $config = new Zend_Config(array('section' => array('a' => array('b' => 'cc')))); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array('section' => array('a' => array('b' => 'cc')))); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertEquals( 'cc', $newConfig->get('section')->get('a')->get('b'), - 'PreservingIniWriter does not update nested section properties' + 'IniWriter does not update nested section properties' ); } @@ -260,50 +290,53 @@ EOD a.b = "c" EOD ); - $config = new Zend_Config(array('section' => array())); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $config = new Config(array('section' => array())); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertNull( $newConfig->get('section')->get('a'), - 'PreservingIniWriter does not delete nested section properties' + 'IniWriter does not delete nested section properties' ); } public function testWhetherSimplePropertiesOfExtendingSectionsAreInserted() { + $this->markTestSkipped( + 'Implementation has changed. There is no "Extend" functionality anymore in our Config object' + ); $target = $this->writeConfigToTemporaryFile(''); - $config = new Zend_Config( + $config = new Config( array( 'foo' => array('key1' => '1'), 'bar' => array('key2' => '2') ) ); $config->setExtend('bar', 'foo'); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertInstanceOf( - '\Zend_Config', + get_class($newConfig), $newConfig->get('foo'), - 'PreservingIniWriter does not insert extended sections' + 'IniWriter does not insert extended sections' ); $this->assertInstanceOf( - '\Zend_Config', + get_class($newConfig), $newConfig->get('bar'), - 'PreservingIniWriter does not insert extending sections' + 'IniWriter does not insert extending sections' ); $this->assertEquals( '2', $newConfig->get('bar')->get('key2'), - 'PreservingIniWriter does not insert simple properties into extending sections' + 'IniWriter does not insert simple properties into extending sections' ); $this->assertEquals( '1', $newConfig->get('foo')->get('key1'), - 'PreservingIniWriter does not properly define extending sections' + 'IniWriter does not properly define extending sections' ); } @@ -312,6 +345,9 @@ EOD */ public function testWhetherSimplePropertiesOfExtendingSectionsAreUpdated() { + $this->markTestSkipped( + 'Implementation has changed. There is no "Extend" functionality anymore in our Config object' + ); $target = $this->writeConfigToTemporaryFile(<<<'EOD' [foo] key1 = "1" @@ -320,21 +356,21 @@ key1 = "1" key2 = "2" EOD ); - $config = new Zend_Config( + $config = new Config( array( 'foo' => array('key1' => '1'), 'bar' => array('key2' => '22') ) ); $config->setExtend('bar', 'foo'); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertEquals( '22', $newConfig->get('bar')->get('key2'), - 'PreservingIniWriter does not update simple properties of extending sections' + 'IniWriter does not update simple properties of extending sections' ); } @@ -343,6 +379,9 @@ EOD */ public function testWhetherSimplePropertiesOfExtendingSectionsAreDeleted() { + $this->markTestSkipped( + 'Implementation has changed. There is no "Extend" functionality anymore in our Config object' + ); $target = $this->writeConfigToTemporaryFile(<<<'EOD' [foo] key1 = "1" @@ -351,61 +390,64 @@ key1 = "1" key2 = "2" EOD ); - $config = new Zend_Config( + $config = new Config( array( 'foo' => array('key1' => '1'), 'bar' => array() ) ); $config->setExtend('bar', 'foo'); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertNull( $newConfig->get('bar')->get('key2'), - 'PreservingIniWriter does not delete simple properties of extending sections' + 'IniWriter does not delete simple properties of extending sections' ); } public function testWhetherNestedPropertiesOfExtendingSectionsAreInserted() { + $this->markTestSkipped( + 'Implementation has changed. There is no "Extend" functionality anymore in our Config object' + ); $target = $this->writeConfigToTemporaryFile(''); - $config = new Zend_Config( + $config = new Config( array( 'foo' => array('a' => array('b' => 'c')), 'bar' => array('d' => array('e' => 'f')) ) ); $config->setExtend('bar', 'foo'); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertInstanceOf( - '\Zend_Config', + get_class($newConfig), $newConfig->get('foo'), - 'PreservingIniWriter does not insert extended sections' + 'IniWriter does not insert extended sections' ); $this->assertInstanceOf( - '\Zend_Config', + get_class($newConfig), $newConfig->get('bar'), - 'PreservingIniWriter does not insert extending sections' + 'IniWriter does not insert extending sections' ); $this->assertInstanceOf( - '\Zend_Config', + get_class($newConfig), $newConfig->get('bar')->get('d'), - 'PreservingIniWriter does not insert nested properties into extending sections' + 'IniWriter does not insert nested properties into extending sections' ); $this->assertEquals( 'f', $newConfig->get('bar')->get('d')->get('e'), - 'PreservingIniWriter does not insert nested properties into extending sections' + 'IniWriter does not insert nested properties into extending sections' ); $this->assertEquals( 'c', $newConfig->get('bar')->get('a')->get('b'), - 'PreservingIniWriter does not properly define extending sections with nested properties' + 'IniWriter does not properly define extending sections with nested properties' ); } @@ -422,21 +464,21 @@ a.b = "c" d.e = "f" EOD ); - $config = new Zend_Config( + $config = new Config( array( 'foo' => array('a' => array('b' => 'c')), 'bar' => array('d' => array('e' => 'ff')) ) ); $config->setExtend('bar', 'foo'); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertEquals( 'ff', $newConfig->get('bar')->get('d')->get('e'), - 'PreservingIniWriter does not update nested properties of extending sections' + 'IniWriter does not update nested properties of extending sections' ); } @@ -445,6 +487,9 @@ EOD */ public function testWhetherNestedPropertiesOfExtendingSectionsAreDeleted() { + $this->markTestSkipped( + 'Implementation has changed. There is no "Extend" functionality anymore in our Config object' + ); $target = $this->writeConfigToTemporaryFile(<<<'EOD' [foo] a.b = "c" @@ -453,20 +498,20 @@ a.b = "c" d.e = "f" EOD ); - $config = new Zend_Config( + $config = new Config( array( 'foo' => array('a' => array('b' => 'c')), 'bar' => array() ) ); $config->setExtend('bar', 'foo'); - $writer = new PreservingIniWriter(array('config' => $config, 'filename' => $target)); + $writer = new IniWriter(array('config' => $config, 'filename' => $target)); $writer->write(); - $newConfig = new Zend_Config_Ini($target); + $newConfig = Config::fromIni($target); $this->assertNull( $newConfig->get('bar')->get('d'), - 'PreservingIniWriter does not delete nested properties of extending sections' + 'IniWriter does not delete nested properties of extending sections' ); } @@ -504,9 +549,9 @@ key1 = "1" key2 = "2" EOD; $target = $this->writeConfigToTemporaryFile($config); - $writer = new PreservingIniWriter( + $writer = new IniWriter( array( - 'config' => new Zend_Config( + 'config' => new Config( array( 'three' => array( 'foo' => array( @@ -535,7 +580,7 @@ EOD; $this->assertEquals( trim($reverted), trim($writer->render()), - 'PreservingIniWriter does not preserve section and/or property order' + 'IniWriter does not preserve section and/or property order' ); } @@ -559,9 +604,9 @@ EOD; [one] EOD; $target = $this->writeConfigToTemporaryFile($config); - $writer = new PreservingIniWriter( + $writer = new IniWriter( array( - 'config' => new Zend_Config( + 'config' => new Config( array( 'two' => array(), 'one' => array() @@ -574,7 +619,7 @@ EOD; $this->assertEquals( trim($reverted), trim($writer->render()), - 'PreservingIniWriter does not preserve section and/or property order' + 'IniWriter does not preserve section and/or property order' ); } @@ -588,14 +633,14 @@ key = "value" ; boring comment EOD; $target = $this->writeConfigToTemporaryFile($config); - $writer = new PreservingIniWriter( - array('config' => new Zend_Config(array('key' => 'value')), 'filename' => $target) + $writer = new IniWriter( + array('config' => new Config(array('key' => 'value')), 'filename' => $target) ); $this->assertEquals( $config, $writer->render(), - 'PreservingIniWriter does not preserve comments on empty lines' + 'IniWriter does not preserve comments on empty lines' ); } @@ -608,9 +653,9 @@ key = "value" ; some comment for a small sized pro xxl = "very loooooooooooooooooooooong" ; my value is very lo... EOD; $target = $this->writeConfigToTemporaryFile($config); - $writer = new PreservingIniWriter( + $writer = new IniWriter( array( - 'config' => new Zend_Config( + 'config' => new Config( array( 'foo' => 1337, 'bar' => 7331, @@ -625,7 +670,7 @@ EOD; $this->assertEquals( $config, $writer->render(), - 'PreservingIniWriter does not preserve comments on property lines' + 'IniWriter does not preserve comments on property lines' ); } @@ -637,14 +682,14 @@ EOD; key = "value" EOD; $target = $this->writeConfigToTemporaryFile($config); - $writer = new PreservingIniWriter( - array('config' => new Zend_Config(array('section' => array('key' => 'value'))), 'filename' => $target) + $writer = new IniWriter( + array('config' => new Config(array('section' => array('key' => 'value'))), 'filename' => $target) ); $this->assertEquals( $config, $writer->render(), - 'PreservingIniWriter does not preserve comments on empty section lines' + 'IniWriter does not preserve comments on empty section lines' ); } @@ -658,9 +703,9 @@ key = "value" ; some comment for a small sized pro xxl = "very loooooooooooooooooooooong" ; my value is very lo... EOD; $target = $this->writeConfigToTemporaryFile($config); - $writer = new PreservingIniWriter( + $writer = new IniWriter( array( - 'config' => new Zend_Config( + 'config' => new Config( array( 'section' => array( 'foo' => 1337, @@ -677,48 +722,10 @@ EOD; $this->assertEquals( $config, $writer->render(), - 'PreservingIniWriter does not preserve comments on property lines' + 'IniWriter does not preserve comments on property lines' ); } - public function testKeyNormalization() - { - $normalKeys = new PreservingIniWriter( - array ( - 'config' => new Zend_Config(array ( - 'foo' => 'bar', - 'nest' => array ( - 'nested' => array ( - 'stuff' => 'nested configuration element' - ) - ), - 'preserving' => array ( - 'ini' => array( - 'writer' => 'n' - ), - 'foo' => 'this should not be overwritten' - ) - )), - 'filename' => $this->tempFile - ) - - ); - - $nestedKeys = new PreservingIniWriter( - array ( - 'config' => new Zend_Config(array ( - 'foo' => 'bar', - 'nest.nested.stuff' => 'nested configuration element', - 'preserving.ini.writer' => 'n', - 'preserving.foo' => 'this should not be overwritten' - )), - 'filename' => $this->tempFile2 - ) - - ); - $this->assertEquals($normalKeys->render(), $nestedKeys->render()); - } - /** * Write a INI-configuration string to a temporary file and return it's path * diff --git a/test/php/library/Icinga/Logger/Writer/StreamWriterTest.php b/test/php/library/Icinga/Logger/Writer/StreamWriterTest.php index 11fec9740..44831a5e7 100644 --- a/test/php/library/Icinga/Logger/Writer/StreamWriterTest.php +++ b/test/php/library/Icinga/Logger/Writer/StreamWriterTest.php @@ -4,10 +4,10 @@ namespace Tests\Icinga\Logger\Writer; -use Zend_Config; -use Icinga\Logger\Logger; +use Icinga\Application\Config; +use Icinga\Application\Logger; +use Icinga\Application\Logger\Writer\FileWriter; use Icinga\Test\BaseTestCase; -use Icinga\Logger\Writer\FileWriter; class StreamWriterTest extends BaseTestCase { @@ -27,7 +27,7 @@ class StreamWriterTest extends BaseTestCase public function testWhetherStreamWriterCreatesMissingFiles() { - new FileWriter(new Zend_Config(array('file' => $this->target))); + new FileWriter(new Config(array('file' => $this->target))); $this->assertFileExists($this->target, 'StreamWriter does not create missing files on initialization'); } @@ -36,7 +36,7 @@ class StreamWriterTest extends BaseTestCase */ public function testWhetherStreamWriterWritesMessages() { - $writer = new FileWriter(new Zend_Config(array('file' => $this->target))); + $writer = new FileWriter(new Config(array('file' => $this->target))); $writer->log(Logger::ERROR, 'This is a test error'); $log = file_get_contents($this->target); $this->assertContains('This is a test error', $log, 'StreamWriter does not write log messages'); diff --git a/test/php/library/Icinga/Protocol/Ldap/QueryTest.php b/test/php/library/Icinga/Protocol/Ldap/QueryTest.php index f3fa2c727..4871abaa0 100644 --- a/test/php/library/Icinga/Protocol/Ldap/QueryTest.php +++ b/test/php/library/Icinga/Protocol/Ldap/QueryTest.php @@ -4,15 +4,15 @@ namespace Tests\Icinga\Protocol\Ldap; -use Zend_Config; use Icinga\Test\BaseTestCase; +use Icinga\Application\Config; use Icinga\Protocol\Ldap\Connection; class QueryTest extends BaseTestCase { private function emptySelect() { - $config = new Zend_Config( + $config = new Config( array( 'hostname' => 'localhost', 'root_dn' => 'dc=example,dc=com', diff --git a/test/php/library/Icinga/User/Store/DbStoreTest.php b/test/php/library/Icinga/User/Store/DbStoreTest.php index d98c3fdfc..37c5f7470 100644 --- a/test/php/library/Icinga/User/Store/DbStoreTest.php +++ b/test/php/library/Icinga/User/Store/DbStoreTest.php @@ -6,8 +6,8 @@ namespace Tests\Icinga\User\Preferences\Store; use Mockery; use Exception; -use Zend_Config; use Icinga\Test\BaseTestCase; +use Icinga\Application\Config; use Icinga\User\Preferences\Store\DbStore; class DatabaseMock @@ -165,7 +165,7 @@ class DbStoreTest extends BaseTestCase protected function getStore($dbMock) { return new DbStoreWithSetPreferences( - new Zend_Config( + new Config( array( 'connection' => Mockery::mock(array('getDbAdapter' => $dbMock)) ) diff --git a/test/php/library/Icinga/User/Store/IniStoreTest.php b/test/php/library/Icinga/User/Store/IniStoreTest.php index d6637d2f2..c3d8827d2 100644 --- a/test/php/library/Icinga/User/Store/IniStoreTest.php +++ b/test/php/library/Icinga/User/Store/IniStoreTest.php @@ -5,8 +5,8 @@ namespace Tests\Icinga\User\Preferences\Store; use Mockery; -use Zend_Config; use Icinga\Test\BaseTestCase; +use Icinga\Application\Config; use Icinga\User\Preferences\Store\IniStore; class IniStoreWithSetGetPreferencesAndEmptyWrite extends IniStore @@ -56,7 +56,7 @@ class IniStoreTest extends BaseTestCase protected function getStore() { return new IniStoreWithSetGetPreferencesAndEmptyWrite( - new Zend_Config( + new Config( array( 'location' => 'some/Path/To/Some/Directory' ) diff --git a/test/php/library/Icinga/Web/MenuTest.php b/test/php/library/Icinga/Web/MenuTest.php index eb6456773..b0727111b 100644 --- a/test/php/library/Icinga/Web/MenuTest.php +++ b/test/php/library/Icinga/Web/MenuTest.php @@ -4,21 +4,21 @@ namespace Tests\Icinga\Web; -use Zend_Config; use Icinga\Web\Menu; use Icinga\Test\BaseTestCase; +use Icinga\Application\Config; class MenuTest extends BaseTestCase { public function testWhetherMenusAreNaturallySorted() { $menu = new Menu('test'); - $menu->addSubMenu(5, new Zend_Config(array('title' => 'ccc5'))); - $menu->addSubMenu(0, new Zend_Config(array('title' => 'aaa'))); - $menu->addSubMenu(3, new Zend_Config(array('title' => 'ccc'))); - $menu->addSubMenu(2, new Zend_Config(array('title' => 'bbb'))); - $menu->addSubMenu(4, new Zend_Config(array('title' => 'ccc2'))); - $menu->addSubMenu(1, new Zend_Config(array('title' => 'bb'))); + $menu->addSubMenu(5, new Config(array('title' => 'ccc5'))); + $menu->addSubMenu(0, new Config(array('title' => 'aaa'))); + $menu->addSubMenu(3, new Config(array('title' => 'ccc'))); + $menu->addSubMenu(2, new Config(array('title' => 'bbb'))); + $menu->addSubMenu(4, new Config(array('title' => 'ccc2'))); + $menu->addSubMenu(1, new Config(array('title' => 'bb'))); $this->assertEquals( array('aaa', 'bb', 'bbb', 'ccc', 'ccc2', 'ccc5'), diff --git a/test/php/library/Icinga/Web/Session/SessionNamespaceTest.php b/test/php/library/Icinga/Web/Session/SessionNamespaceTest.php index 11dcb23ec..bf984dca0 100644 --- a/test/php/library/Icinga/Web/Session/SessionNamespaceTest.php +++ b/test/php/library/Icinga/Web/Session/SessionNamespaceTest.php @@ -4,7 +4,6 @@ namespace Tests\Icinga\Web\Session; -use Mockery; use Icinga\Test\BaseTestCase; use Icinga\Web\Session\SessionNamespace; @@ -86,25 +85,39 @@ class SessionNamespaceTest extends BaseTestCase } } - /** - * @expectedException Icinga\Exception\IcingaException - * @expectedExceptionMessage Cannot save, session not set - */ - public function testInvalidParentWrite() + public function testRetrievingValuesByReferenceWorks() { $ns = new SessionNamespace(); - $ns->write(); + $ns->array = array(1, 2); + $array = & $ns->getByRef('array'); + $array[0] = 11; + + $this->assertEquals( + array(11, 2), + $ns->array, + 'Values retrieved with getByRef() seem not be affected by external changes' + ); } - /** - * Check whether it is possible to write a namespace's parent - */ - public function testValidParentWrite() + public function testSettingValuesByReferenceWorks() { - $sessionMock = Mockery::mock('Icinga\Web\Session\Session'); - $sessionMock->shouldReceive('write')->atLeast()->times(1); + $ns = new SessionNamespace(); + $array = array(1, 2); + $ns->setByRef('array', $array); + $array[0] = 11; - $ns = new SessionNamespace($sessionMock); - $ns->write(); + $this->assertEquals( + array(11, 2), + $ns->array, + 'Values set with setByRef() seem not to receive external changes' + ); + } + + public function testTrackingChangesWorks() + { + $ns = new SessionNamespace(); + $this->assertFalse($ns->hasChanged(), 'A new empty session namespace seems to have changes'); + $ns->test = 1; + $this->assertTrue($ns->hasChanged(), 'A new session namespace with values seems not to have changes'); } }