diff --git a/.gitattributes b/.gitattributes index a05c2f5cd..6ae8ee7af 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,6 @@ Vagrantfile export-ignore # Normalize puppet manifests' line endings to LF on checkin and prevent conversion to CRLF when the files are checked out .puppet* eol=lf + +# Include version information on `git archive' +/application/VERSION export-subst diff --git a/application/VERSION b/application/VERSION new file mode 100644 index 000000000..519b667a7 --- /dev/null +++ b/application/VERSION @@ -0,0 +1 @@ +$Format:%H%d %ci$ diff --git a/application/controllers/AboutController.php b/application/controllers/AboutController.php new file mode 100644 index 000000000..a4247f234 --- /dev/null +++ b/application/controllers/AboutController.php @@ -0,0 +1,15 @@ +view->version = Version::get(); + } +} diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 8f2c33e62..a4161981a 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -28,6 +28,10 @@ class GroupController extends AuthBackendController function ($b) { return $b->getName(); }, $this->loadUserGroupBackends('Icinga\Data\Selectable') ); + if (empty($backendNames)) { + return; + } + $this->view->backendSelection = new Form(); $this->view->backendSelection->setAttrib('class', 'backend-selection'); $this->view->backendSelection->setUidDisabled(); @@ -100,6 +104,7 @@ class GroupController extends AuthBackendController $filterEditor = Widget::create('filterEditor') ->setQuery($members) + ->setSearchColumns(array('user')) ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'group') ->ignoreParams('page') ->handleRequest($this->getRequest()); diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index e944af52c..54482dd7d 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -28,6 +28,10 @@ class UserController extends AuthBackendController function ($b) { return $b->getName(); }, $this->loadUserBackends('Icinga\Data\Selectable') ); + if (empty($backendNames)) { + return; + } + $this->view->backendSelection = new Form(); $this->view->backendSelection->setAttrib('class', 'backend-selection'); $this->view->backendSelection->setUidDisabled(); @@ -99,6 +103,7 @@ class UserController extends AuthBackendController $filterEditor = Widget::create('filterEditor') ->setQuery($memberships) + ->setSearchColumns(array('group_name')) ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'user') ->ignoreParams('page') ->handleRequest($this->getRequest()); diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php index cdb6826be..1a3c2f46e 100644 --- a/application/controllers/UsergroupbackendController.php +++ b/application/controllers/UsergroupbackendController.php @@ -52,7 +52,7 @@ class UsergroupbackendController extends Controller $form->setIniConfig(Config::app('groups')); $form->setOnSuccess(function (UserGroupBackendForm $form) { try { - $form->add($form->getValues()); + $form->add(array_filter($form->getValues())); } catch (Exception $e) { $form->error($e->getMessage()); return false; @@ -85,7 +85,12 @@ class UsergroupbackendController extends Controller $form->setIniConfig(Config::app('groups')); $form->setOnSuccess(function (UserGroupBackendForm $form) use ($backendName) { try { - $form->edit($backendName, $form->getValues()); + $form->edit($backendName, array_map( + function ($v) { + return $v !== '' ? $v : null; + }, + $form->getValues() + )); } catch (Exception $e) { $form->error($e->getMessage()); return false; diff --git a/application/forms/Config/UserBackend/LdapBackendForm.php b/application/forms/Config/UserBackend/LdapBackendForm.php index c520ec68f..ac2091398 100644 --- a/application/forms/Config/UserBackend/LdapBackendForm.php +++ b/application/forms/Config/UserBackend/LdapBackendForm.php @@ -8,7 +8,7 @@ use Icinga\Web\Form; use Icinga\Data\ConfigObject; use Icinga\Data\ResourceFactory; use Icinga\Exception\AuthenticationException; -use Icinga\Authentication\User\LdapUserBackend; +use Icinga\Authentication\User\UserBackend; /** * Form class for adding/modifying LDAP user backends @@ -48,6 +48,8 @@ class LdapBackendForm extends Form */ public function createElements(array $formData) { + $isAd = isset($formData['type']) ? $formData['type'] === 'msldap' : false; + $this->addElement( 'text', 'name', @@ -77,10 +79,13 @@ class LdapBackendForm extends Form 'text', 'user_class', array( - 'required' => true, - 'label' => $this->translate('LDAP User Object Class'), - 'description' => $this->translate('The object class used for storing users on the LDAP server.'), - 'value' => 'inetOrgPerson' + 'preserveDefault' => true, + 'required' => ! $isAd, + 'ignore' => $isAd, + 'disabled' => $isAd ?: null, + 'label' => $this->translate('LDAP User Object Class'), + 'description' => $this->translate('The object class used for storing users on the LDAP server.'), + 'value' => $isAd ? 'user' : 'inetOrgPerson' ) ); $this->addElement( @@ -117,12 +122,15 @@ class LdapBackendForm extends Form 'text', 'user_name_attribute', array( - 'required' => true, - 'label' => $this->translate('LDAP User Name Attribute'), - 'description' => $this->translate( + 'preserveDefault' => true, + 'required' => ! $isAd, + 'ignore' => $isAd, + 'disabled' => $isAd ?: null, + 'label' => $this->translate('LDAP User Name Attribute'), + 'description' => $this->translate( 'The attribute name used for storing the user name on the LDAP server.' ), - 'value' => 'uid' + 'value' => $isAd ? 'sAMAccountName' : 'uid' ) ); $this->addElement( @@ -130,7 +138,7 @@ class LdapBackendForm extends Form 'backend', array( 'disabled' => true, - 'value' => 'ldap' + 'value' => $isAd ? 'msldap' : 'ldap' ) ); $this->addElement( @@ -170,8 +178,7 @@ class LdapBackendForm extends Form public static function isValidUserBackend(Form $form) { try { - $ldapUserBackend = new LdapUserBackend(ResourceFactory::createResource($form->getResourceConfig())); - $ldapUserBackend->setConfig(new ConfigObject($form->getValues())); + $ldapUserBackend = UserBackend::create(null, new ConfigObject($form->getValues())); $ldapUserBackend->assertAuthenticationPossible(); } catch (AuthenticationException $e) { if (($previous = $e->getPrevious()) !== null) { @@ -193,6 +200,8 @@ class LdapBackendForm extends Form * Return the configuration for the chosen resource * * @return ConfigObject + * + * @todo Check whether it's possible to drop this (Or even all occurences!) */ public function getResourceConfig() { diff --git a/application/forms/Config/UserBackendConfigForm.php b/application/forms/Config/UserBackendConfigForm.php index 0a30dd590..62a68e70b 100644 --- a/application/forms/Config/UserBackendConfigForm.php +++ b/application/forms/Config/UserBackendConfigForm.php @@ -60,16 +60,24 @@ class UserBackendConfigForm extends ConfigForm */ public function getBackendForm($type) { - if ($type === 'db') { - $form = new DbBackendForm(); - $form->setResources(isset($this->resources['db']) ? $this->resources['db'] : array()); - } elseif ($type === 'ldap') { - $form = new LdapBackendForm(); - $form->setResources(isset($this->resources['ldap']) ? $this->resources['ldap'] : array()); - } elseif ($type === 'external') { - $form = new ExternalBackendForm(); - } else { - throw new InvalidArgumentException(sprintf($this->translate('Invalid backend type "%s" provided'), $type)); + switch ($type) + { + case 'db': + $form = new DbBackendForm(); + $form->setResources(isset($this->resources['db']) ? $this->resources['db'] : array()); + break; + case 'ldap': + case 'msldap': + $form = new LdapBackendForm(); + $form->setResources(isset($this->resources['ldap']) ? $this->resources['ldap'] : array()); + break; + case 'external': + $form = new ExternalBackendForm(); + break; + default: + throw new InvalidArgumentException( + sprintf($this->translate('Invalid backend type "%s" provided'), $type) + ); } return $form; @@ -296,6 +304,7 @@ class UserBackendConfigForm extends ConfigForm } if (isset($this->resources['ldap']) && ($backendType === 'ldap' || Platform::extensionLoaded('ldap'))) { $backendTypes['ldap'] = 'LDAP'; + $backendTypes['msldap'] = 'ActiveDirectory'; } $externalBackends = array_filter( diff --git a/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php new file mode 100644 index 000000000..c3c8baacf --- /dev/null +++ b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php @@ -0,0 +1,310 @@ +setName('form_config_ldapusergroupbackend'); + } + + /** + * Create and add elements to this form + * + * @param array $formData + */ + public function createElements(array $formData) + { + $resourceNames = $this->getLdapResourceNames(); + $this->addElement( + 'select', + 'resource', + array( + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('LDAP Connection'), + 'description' => $this->translate('The LDAP connection to use for this backend.'), + 'multiOptions' => array_combine($resourceNames, $resourceNames) + ) + ); + $resource = ResourceFactory::create( + isset($formData['resource']) && in_array($formData['resource'], $resourceNames) + ? $formData['resource'] + : $resourceNames[0] + ); + + $userBackends = array('none' => $this->translate('None', 'usergroupbackend.ldap.user_backend')); + $userBackendNames = $this->getLdapUserBackendNames($resource); + if (! empty($userBackendNames)) { + $userBackends = array_merge($userBackends, array_combine($userBackendNames, $userBackendNames)); + } + $this->addElement( + 'select', + 'user_backend', + array( + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('User Backend'), + 'description' => $this->translate('The user backend to link with this user group backend.'), + 'multiOptions' => $userBackends + ) + ); + + $groupBackend = new LdapUserGroupBackend($resource); + if ($formData['type'] === 'ldap') { + $defaults = $groupBackend->getOpenLdapDefaults(); + $groupConfigDisabled = $userConfigDisabled = null; // MUST BE null, do NOT change this to false! + } else { // $formData['type'] === 'msldap' + $defaults = $groupBackend->getActiveDirectoryDefaults(); + $groupConfigDisabled = $userConfigDisabled = true; + } + + $dnDisabled = null; // MUST BE null + if (isset($formData['user_backend']) && $formData['user_backend'] !== 'none') { + $userBackend = UserBackend::create($formData['user_backend']); + $defaults->merge(array( + 'user_base_dn' => $userBackend->getBaseDn(), + 'user_class' => $userBackend->getUserClass(), + 'user_name_attribute' => $userBackend->getUserNameAttribute(), + 'user_filter' => $userBackend->getFilter() + )); + $userConfigDisabled = $dnDisabled = true; + } + + $this->createGroupConfigElements($defaults, $groupConfigDisabled); + $this->createUserConfigElements($defaults, $userConfigDisabled, $dnDisabled); + } + + /** + * Create and add all elements to this form required for the group configuration + * + * @param ConfigObject $defaults + * @param null|bool $disabled + */ + protected function createGroupConfigElements(ConfigObject $defaults, $disabled) + { + $this->addElement( + 'text', + 'group_class', + array( + 'preserveDefault' => true, + 'ignore' => $disabled, + 'disabled' => $disabled, + 'label' => $this->translate('LDAP Group Object Class'), + 'description' => $this->translate('The object class used for storing groups on the LDAP server.'), + 'value' => $defaults->group_class + ) + ); + $this->addElement( + 'text', + 'group_filter', + array( + 'preserveDefault' => true, + 'allowEmpty' => true, + 'label' => $this->translate('LDAP Group Filter'), + 'description' => $this->translate( + 'An additional filter to use when looking up groups using the specified connection. ' + . 'Leave empty to not to use any additional filter rules.' + ), + 'requirement' => $this->translate( + 'The filter needs to be expressed as standard LDAP expression, without' + . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)' + ), + 'validators' => array( + array( + 'Callback', + false, + array( + 'callback' => function ($v) { + return strpos($v, '(') !== 0; + }, + 'messages' => array( + 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.') + ) + ) + ) + ), + 'value' => $defaults->group_filter + ) + ); + $this->addElement( + 'text', + 'group_name_attribute', + array( + 'preserveDefault' => true, + 'ignore' => $disabled, + 'disabled' => $disabled, + 'label' => $this->translate('LDAP Group Name Attribute'), + 'description' => $this->translate( + 'The attribute name used for storing a group\'s name on the LDAP server.' + ), + 'value' => $defaults->group_name_attribute + ) + ); + $this->addElement( + 'text', + 'base_dn', + array( + 'preserveDefault' => true, + 'label' => $this->translate('LDAP Group Base DN'), + 'description' => $this->translate( + 'The path where groups can be found on the LDAP server. Leave ' . + 'empty to select all users available using the specified connection.' + ), + 'value' => $defaults->base_dn + ) + ); + } + + /** + * Create and add all elements to this form required for the user configuration + * + * @param ConfigObject $defaults + * @param null|bool $disabled + * @param null|bool $dnDisabled + */ + protected function createUserConfigElements(ConfigObject $defaults, $disabled, $dnDisabled) + { + $this->addElement( + 'text', + 'user_class', + array( + 'preserveDefault' => true, + 'ignore' => $disabled, + 'disabled' => $disabled, + 'label' => $this->translate('LDAP User Object Class'), + 'description' => $this->translate('The object class used for storing users on the LDAP server.'), + 'value' => $defaults->user_class + ) + ); + $this->addElement( + 'text', + 'user_filter', + array( + 'preserveDefault' => true, + 'allowEmpty' => true, + 'ignore' => $dnDisabled, + 'disabled' => $dnDisabled, + 'label' => $this->translate('LDAP User Filter'), + 'description' => $this->translate( + 'An additional filter to use when looking up users using the specified connection. ' + . 'Leave empty to not to use any additional filter rules.' + ), + 'requirement' => $this->translate( + 'The filter needs to be expressed as standard LDAP expression, without' + . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)' + ), + 'validators' => array( + array( + 'Callback', + false, + array( + 'callback' => function ($v) { + return strpos($v, '(') !== 0; + }, + 'messages' => array( + 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.') + ) + ) + ) + ), + 'value' => $defaults->user_filter + ) + ); + $this->addElement( + 'text', + 'user_name_attribute', + array( + 'preserveDefault' => true, + 'ignore' => $disabled, + 'disabled' => $disabled, + 'label' => $this->translate('LDAP User Name Attribute'), + 'description' => $this->translate( + 'The attribute name used for storing a user\'s name on the LDAP server.' + ), + 'value' => $defaults->user_name_attribute + ) + ); + $this->addElement( + 'text', + 'user_base_dn', + array( + 'preserveDefault' => true, + 'ignore' => $dnDisabled, + 'disabled' => $dnDisabled, + 'label' => $this->translate('LDAP User Base DN'), + 'description' => $this->translate( + 'The path where users can be found on the LDAP server. Leave ' . + 'empty to select all users available using the specified connection.' + ), + 'value' => $defaults->user_base_dn + ) + ); + } + + /** + * Return the names of all configured LDAP resources + * + * @return array + */ + protected function getLdapResourceNames() + { + $names = array(); + foreach (ResourceFactory::getResourceConfigs() as $name => $config) { + if (in_array(strtolower($config->type), array('ldap', 'msldap'))) { + $names[] = $name; + } + } + + if (empty($names)) { + Notification::error( + $this->translate('No LDAP resources available. Please configure an LDAP resource first.') + ); + $this->getResponse()->redirectAndExit('config/createresource'); + } + + return $names; + } + + /** + * Return the names of all configured LDAP user backends + * + * @param Connection $resource + * + * @return array + */ + protected function getLdapUserBackendNames(Connection $resource) + { + $names = array(); + foreach (Config::app('authentication') as $name => $config) { + if (in_array(strtolower($config->backend), array('ldap', 'msldap'))) { + $backendResource = ResourceFactory::create($config->resource); + if ( + $backendResource->getHostname() === $resource->getHostname() + && $backendResource->getPort() === $resource->getPort() + ) { + $names[] = $name; + } + } + } + + return $names; + } +} diff --git a/application/forms/Config/UserGroup/UserGroupBackendForm.php b/application/forms/Config/UserGroup/UserGroupBackendForm.php index dd9875c9a..5bd0d9681 100644 --- a/application/forms/Config/UserGroup/UserGroupBackendForm.php +++ b/application/forms/Config/UserGroup/UserGroupBackendForm.php @@ -13,6 +13,13 @@ use Icinga\Forms\ConfigForm; */ class UserGroupBackendForm extends ConfigForm { + /** + * The backend to load when displaying the form for the first time + * + * @var string + */ + protected $backendToLoad; + /** * Initialize this form */ @@ -31,10 +38,17 @@ class UserGroupBackendForm extends ConfigForm */ public function getBackendForm($type) { - if ($type === 'db') { - return new DbUserGroupBackendForm(); - } else { - throw new InvalidArgumentException(sprintf($this->translate('Invalid backend type "%s" provided'), $type)); + switch ($type) + { + case 'db': + return new DbUserGroupBackendForm(); + case 'ldap': + case 'msldap': + return new LdapUserGroupBackendForm(); + default: + throw new InvalidArgumentException( + sprintf($this->translate('Invalid backend type "%s" provided'), $type) + ); } } @@ -53,10 +67,7 @@ class UserGroupBackendForm extends ConfigForm throw new NotFoundError('No user group backend called "%s" found', $name); } - $data = $this->config->getSection($name)->toArray(); - $data['type'] = $data['backend']; - $data['name'] = $name; - $this->populate($data); + $this->backendToLoad = $name; return $this; } @@ -103,13 +114,23 @@ class UserGroupBackendForm extends ConfigForm } $backendConfig = $this->config->getSection($name); - if (isset($data['name']) && $data['name'] !== $name) { - $this->config->removeSection($name); - $name = $data['name']; + if (isset($data['name'])) { + if ($data['name'] !== $name) { + $this->config->removeSection($name); + $name = $data['name']; + } + unset($data['name']); } - $this->config->setSection($name, $backendConfig->merge($data)); + $backendConfig->merge($data); + foreach ($backendConfig->toArray() as $k => $v) { + if ($v === null) { + unset($backendConfig->$k); + } + } + + $this->config->setSection($name, $backendConfig); return $this; } @@ -161,7 +182,9 @@ class UserGroupBackendForm extends ConfigForm // TODO(jom): We did not think about how to configure custom group backends yet! $backendTypes = array( - 'db' => $this->translate('Database') + 'db' => $this->translate('Database'), + 'ldap' => 'LDAP', + 'msldap' => 'ActiveDirectory' ); $backendType = isset($formData['type']) ? $formData['type'] : null; @@ -191,8 +214,34 @@ class UserGroupBackendForm extends ConfigForm ) ); - $backendForm = $this->getBackendForm($backendType); - $backendForm->createElements($formData); - $this->addElements($backendForm->getElements()); + $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form'); + } + + /** + * Populate the configuration of the backend to load + */ + public function onRequest() + { + if ($this->backendToLoad) { + $data = $this->config->getSection($this->backendToLoad)->toArray(); + $data['type'] = $data['backend']; + $data['name'] = $this->backendToLoad; + $this->populate($data); + } + } + + /** + * Retrieve all form element values + * + * @param bool $suppressArrayNotation Ignored + * + * @return array + */ + public function getValues($suppressArrayNotation = false) + { + $values = parent::getValues(); + $values = array_merge($values, $values['backend_form']); + unset($values['backend_form']); + return $values; } } diff --git a/application/views/scripts/about/index.phtml b/application/views/scripts/about/index.phtml new file mode 100644 index 000000000..e270a8eb9 --- /dev/null +++ b/application/views/scripts/about/index.phtml @@ -0,0 +1,25 @@ +
+

Icinga Web 2

+ $this->translate('Version: %s'), + 'gitCommitID' => $this->translate('Git commit ID: %s'), + 'gitCommitDate' => $this->translate('Git commit date: %s') + ) as $key => $label) { + if (array_key_exists($key, $version) && null !== ($value = $version[$key])) { + $versionInfo[] = sprintf($label, htmlspecialchars($value)); + } + } + } + + echo ( + 0 === count($versionInfo) + ? '

' . $this->translate( + 'Can\'t determine Icinga Web 2\'s version' + ) + : '

' . nl2br(implode("\n", $versionInfo), false) + ) . '

'; + ?> +
diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml index bcd9dca93..65c1591cd 100644 --- a/application/views/scripts/group/list.phtml +++ b/application/views/scripts/group/list.phtml @@ -18,7 +18,7 @@ if (! $this->compact): ?>
translate('No backend found which is able to list groups') . '
'; return; } else { diff --git a/application/views/scripts/role/list.phtml b/application/views/scripts/role/list.phtml index 766ba26f3..568bbdf98 100644 --- a/application/views/scripts/role/list.phtml +++ b/application/views/scripts/role/list.phtml @@ -67,7 +67,7 @@ - + translate('Create a New Role') ?> diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index 76a6f2b8b..df9a14a45 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -18,7 +18,7 @@ if (! $this->compact): ?>
translate('No backend found which is able to list users') . '
'; return; } else { diff --git a/icingaweb2.spec b/icingaweb2.spec index 1ea69451d..c7aa4cb8d 100644 --- a/icingaweb2.spec +++ b/icingaweb2.spec @@ -218,6 +218,7 @@ rm -rf %{buildroot} %{basedir}/application/forms %{basedir}/application/layouts %{basedir}/application/views +%{basedir}/application/VERSION %{basedir}/doc %{basedir}/modules %{basedir}/public diff --git a/library/Icinga/Application/Platform.php b/library/Icinga/Application/Platform.php index 355311b97..cc72857f9 100644 --- a/library/Icinga/Application/Platform.php +++ b/library/Icinga/Application/Platform.php @@ -59,6 +59,122 @@ class Platform return strtoupper(substr(self::getOperatingSystemName(), 0, 5)) === 'LINUX'; } + /** + * Return the Linux distribution's name + * or 'linux' if the name could not be found out + * or false if the OS isn't Linux or an error occurred + * + * @param int $reliable + * 3: Only parse /etc/os-release (or /usr/lib/os-release). + * For the paranoid ones. + * 2: If that (3) doesn't help, check /etc/*-release, too. + * If something is unclear, return 'linux'. + * 1: Almost equal to mode 2. The possible return values also include: + * 'redhat' -- unclear whether RHEL/Fedora/... + * 'suse' -- unclear whether SLES/openSUSE/... + * 0: If even that (1) doesn't help, check /proc/version, too. + * This may not work (as expected) on LXC containers! + * (No reliability at all!) + * + * @return string|bool + */ + public static function getLinuxDistro($reliable = 2) + { + if (! self::isLinux()) { + return false; + } + + foreach (array('/etc/os-release', '/usr/lib/os-release') as $osReleaseFile) { + if (false === ($osRelease = @file( + $osReleaseFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES + ))) { + continue; + } + + foreach ($osRelease as $osInfo) { + if (false === ($res = @preg_match('/(? 2) { + return 'linux'; + } + + foreach (array( + 'fedora' => '/etc/fedora-release', + 'centos' => '/etc/centos-release' + ) as $distro => $releaseFile) { + if (! (false === ( + $release = @file_get_contents($releaseFile) + ) || false === strpos(strtolower($release), $distro))) { + return $distro; + } + } + + if (false !== ($release = @file_get_contents('/etc/redhat-release'))) { + $release = strtolower($release); + if (false !== strpos($release, 'red hat enterprise linux')) { + return 'rhel'; + } + foreach (array('fedora', 'centos') as $distro) { + if (false !== strpos($release, $distro)) { + return $distro; + } + } + return $reliable < 2 ? 'redhat' : 'linux'; + } + + if (false !== ($release = @file_get_contents('/etc/SuSE-release'))) { + $release = strtolower($release); + foreach (array( + 'opensuse' => 'opensuse', + 'sles' => 'suse linux enterprise server', + 'sled' => 'suse linux enterprise desktop' + ) as $distro => $name) { + if (false !== strpos($release, $name)) { + return $distro; + } + } + return $reliable < 2 ? 'suse' : 'linux'; + } + + if ($reliable < 1) { + if (false === ($procVersion = @file_get_contents('/proc/version'))) { + return false; + } + $procVersion = strtolower($procVersion); + foreach (array( + 'redhat' => 'red hat', + 'suse' => 'suse linux', + 'ubuntu' => 'ubuntu', + 'debian' => 'debian' + ) as $distro => $name) { + if (false !== strpos($procVersion, $name)) { + return $distro; + } + } + } + + return 'linux'; + } + /** * Test of CLI environment * diff --git a/library/Icinga/Application/Version.php b/library/Icinga/Application/Version.php new file mode 100644 index 000000000..65b6138d9 --- /dev/null +++ b/library/Icinga/Application/Version.php @@ -0,0 +1,37 @@ +getApplicationDir() . DIRECTORY_SEPARATOR . 'VERSION' + ))) { + return false; + } + + $matches = array(); + if (false === ($res = preg_match( + '/(?\w+)(?:\s*\(.*?(?:(?<=[\(,])\s*tag\s*:\s*v(?P.+?)\s*(?=[\),]).*?)?\))?\s*(?P\S+)/ms', + $appVersion, + $matches + )) || $res === 0) { + return false; + } + + foreach ($matches as $key => $value) { + if (is_int($key) || $value === '') { + unset($matches[$key]); + } + } + return $matches; + } +} diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php index 154a33a44..333969891 100644 --- a/library/Icinga/Authentication/User/LdapUserBackend.php +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -4,17 +4,16 @@ namespace Icinga\Authentication\User; use DateTime; -use Icinga\Application\Logger; use Icinga\Data\ConfigObject; use Icinga\Exception\AuthenticationException; use Icinga\Exception\ProgrammingError; -use Icinga\Repository\Repository; +use Icinga\Repository\LdapRepository; use Icinga\Repository\RepositoryQuery; use Icinga\Protocol\Ldap\Exception as LdapException; use Icinga\Protocol\Ldap\Expression; use Icinga\User; -class LdapUserBackend extends Repository implements UserBackendInterface +class LdapUserBackend extends LdapRepository implements UserBackendInterface { /** * The base DN to use for a query @@ -65,20 +64,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface ) ); - protected $groupOptions; - - /** - * Normed attribute names based on known LDAP environments - * - * @var array - */ - protected $normedAttributes = array( - 'uid' => 'uid', - 'user' => 'user', - 'inetorgperson' => 'inetOrgPerson', - 'samaccountname' => 'sAMAccountName' - ); - /** * Set the base DN to use for a query * @@ -179,34 +164,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface return $this->filter; } - public function setGroupOptions(array $options) - { - $this->groupOptions = $options; - return $this; - } - - public function getGroupOptions() - { - return $this->groupOptions; - } - - /** - * Return the given attribute name normed to known LDAP enviroments, if possible - * - * @param string $name - * - * @return string - */ - protected function getNormedAttribute($name) - { - $loweredName = strtolower($name); - if (array_key_exists($loweredName, $this->normedAttributes)) { - return $this->normedAttributes[$loweredName]; - } - - return $name; - } - /** * Apply the given configuration to this backend * @@ -325,37 +282,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface return ((int) $value & $ADS_UF_ACCOUNTDISABLE) === 0; } - /** - * Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation - * - * @param string|null $value - * - * @return int - */ - protected function retrieveGeneralizedTime($value) - { - if ($value === null) { - return $value; - } - - if ( - ($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false - ) { - return $dateTime->getTimeStamp(); - } else { - Logger::debug(sprintf( - 'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) for user backend "%s".', - $value, - $this->getName() - )); - } - } - /** * Return whether the given shadowExpire value defines that a user is permitted to login * @@ -413,41 +339,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface } } - /** - * Retrieve the user groups - * - * @TODO: Subject to change, see #7343 - * - * @param string $dn - * - * @return array - */ - public function getGroups($dn) - { - if (empty($this->groupOptions) || ! isset($this->groupOptions['group_base_dn'])) { - return array(); - } - - $result = $this->ds->select() - ->setBase($this->groupOptions['group_base_dn']) - ->from( - $this->groupOptions['group_class'], - array($this->groupOptions['group_attribute']) - ) - ->where( - $this->groupOptions['group_member_attribute'], - $dn - ) - ->fetchAll(); - - $groups = array(); - foreach ($result as $group) { - $groups[] = $group->{$this->groupOptions['group_attribute']}; - } - - return $groups; - } - /** * Authenticate the given user * @@ -472,15 +363,7 @@ class LdapUserBackend extends Repository implements UserBackendInterface return false; } - $authenticated = $this->ds->testCredentials($userDn, $password); - if ($authenticated) { - $groups = $this->getGroups($userDn); - if ($groups !== null) { - $user->setGroups($groups); - } - } - - return $authenticated; + return $this->ds->testCredentials($userDn, $password); } catch (LdapException $e) { throw new AuthenticationException( 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:', diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php index 3d11289fb..7afd561a7 100644 --- a/library/Icinga/Authentication/User/UserBackend.php +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -3,6 +3,7 @@ namespace Icinga\Authentication\User; +use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Application\Icinga; use Icinga\Data\ConfigObject; @@ -106,8 +107,17 @@ class UserBackend * * @throws ConfigurationError */ - public static function create($name, ConfigObject $backendConfig) + public static function create($name, ConfigObject $backendConfig = null) { + if ($backendConfig === null) { + $authConfig = Config::app('authentication'); + if ($authConfig->hasSection($name)) { + $backendConfig = $authConfig->getSection($name); + } else { + throw new ConfigurationError('User backend "%s" does not exist', $name); + } + } + if ($backendConfig->name !== null) { $name = $backendConfig->name; } @@ -165,12 +175,6 @@ class UserBackend $backend->setUserClass($backendConfig->get('user_class', 'user')); $backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'sAMAccountName')); $backend->setFilter($backendConfig->filter); - $backend->setGroupOptions(array( - 'group_base_dn' => $backendConfig->get('group_base_dn', $resource->getDN()), - 'group_attribute' => $backendConfig->get('group_attribute', 'sAMAccountName'), - 'group_member_attribute' => $backendConfig->get('group_member_attribute', 'member'), - 'group_class' => $backendConfig->get('group_class', 'group') - )); break; case 'ldap': $backend = new LdapUserBackend($resource); @@ -178,12 +182,6 @@ class UserBackend $backend->setUserClass($backendConfig->get('user_class', 'inetOrgPerson')); $backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'uid')); $backend->setFilter($backendConfig->filter); - $backend->setGroupOptions(array( - 'group_base_dn' => $backendConfig->group_base_dn, - 'group_attribute' => $backendConfig->group_attribute, - 'group_member_attribute' => $backendConfig->group_member_attribute, - 'group_class' => $backendConfig->group_class - )); break; } diff --git a/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php new file mode 100644 index 000000000..224013e9f --- /dev/null +++ b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php @@ -0,0 +1,627 @@ + array( + 'order' => 'asc' + ) + ); + + /** + * Normed attribute names based on known LDAP environments + * + * @var array + */ + protected $normedAttributes = array( + 'uid' => 'uid', + 'gid' => 'gid', + 'user' => 'user', + 'group' => 'group', + 'member' => 'member', + 'inetorgperson' => 'inetOrgPerson', + 'samaccountname' => 'sAMAccountName' + ); + + /** + * The name of this repository + * + * @var string + */ + protected $name; + + /** + * The datasource being used + * + * @var Connection + */ + protected $ds; + + /** + * Create a new LDAP repository object + * + * @param Connection $ds The data source to use + */ + public function __construct($ds) + { + $this->ds = $ds; + } + + /** + * Return the given attribute name normed to known LDAP enviroments, if possible + * + * @param string $name + * + * @return string + */ + protected function getNormedAttribute($name) + { + $loweredName = strtolower($name); + if (array_key_exists($loweredName, $this->normedAttributes)) { + return $this->normedAttributes[$loweredName]; + } + + return $name; + } + + /** + * Set this repository's name + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Return this repository's name + * + * In case no name has been explicitly set yet, the class name is returned. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set the base DN to use for a user query + * + * @param string $baseDn + * + * @return $this + */ + public function setUserBaseDn($baseDn) + { + if (($baseDn = trim($baseDn))) { + $this->userBaseDn = $baseDn; + } + + return $this; + } + + /** + * Return the base DN to use for a user query + * + * @return string + */ + public function getUserBaseDn() + { + return $this->userBaseDn; + } + + /** + * Set the base DN to use for a group query + * + * @param string $baseDn + * + * @return $this + */ + public function setGroupBaseDn($baseDn) + { + if (($baseDn = trim($baseDn))) { + $this->groupBaseDn = $baseDn; + } + + return $this; + } + + /** + * Return the base DN to use for a group query + * + * @return string + */ + public function getGroupBaseDn() + { + return $this->groupBaseDn; + } + + /** + * Set the objectClass where to look for users + * + * @param string $userClass + * + * @return $this + */ + public function setUserClass($userClass) + { + $this->userClass = $this->getNormedAttribute($userClass); + return $this; + } + + /** + * Return the objectClass where to look for users + * + * @return string + */ + public function getUserClass() + { + return $this->userClass; + } + + /** + * Set the objectClass where to look for groups + * + * Sets also the base table name for the underlying repository. + * + * @param string $groupClass + * + * @return $this + */ + public function setGroupClass($groupClass) + { + $this->baseTable = $this->groupClass = $this->getNormedAttribute($groupClass); + return $this; + } + + /** + * Return the objectClass where to look for groups + * + * @return string + */ + public function getGroupClass() + { + return $this->groupClass; + } + + /** + * Set the attribute name where to find a user's name + * + * @param string $userNameAttribute + * + * @return $this + */ + public function setUserNameAttribute($userNameAttribute) + { + $this->userNameAttribute = $this->getNormedAttribute($userNameAttribute); + return $this; + } + + /** + * Return the attribute name where to find a user's name + * + * @return string + */ + public function getUserNameAttribute() + { + return $this->userNameAttribute; + } + + /** + * Set the attribute name where to find a group's name + * + * @param string $groupNameAttribute + * + * @return $this + */ + public function setGroupNameAttribute($groupNameAttribute) + { + $this->groupNameAttribute = $this->getNormedAttribute($groupNameAttribute); + return $this; + } + + /** + * Return the attribute name where to find a group's name + * + * @return string + */ + public function getGroupNameAttribute() + { + return $this->groupNameAttribute; + } + + /** + * Set the attribute name where to find a group's member + * + * @param string $groupMemberAttribute + * + * @return $this + */ + public function setGroupMemberAttribute($groupMemberAttribute) + { + $this->groupMemberAttribute = $this->getNormedAttribute($groupMemberAttribute); + return $this; + } + + /** + * Return the attribute name where to find a group's member + * + * @return string + */ + public function getGroupMemberAttribute() + { + return $this->groupMemberAttribute; + } + + /** + * Set the custom LDAP filter to apply on a user query + * + * @param string $filter + * + * @return $this + */ + public function setUserFilter($filter) + { + if (($filter = trim($filter))) { + $this->userFilter = $filter; + } + + return $this; + } + + /** + * Return the custom LDAP filter to apply on a user query + * + * @return string + */ + public function getUserFilter() + { + return $this->userFilter; + } + + /** + * Set the custom LDAP filter to apply on a group query + * + * @param string $filter + * + * @return $this + */ + public function setGroupFilter($filter) + { + if (($filter = trim($filter))) { + $this->groupFilter = $filter; + } + + return $this; + } + + /** + * Return the custom LDAP filter to apply on a group query + * + * @return string + */ + public function getGroupFilter() + { + return $this->groupFilter; + } + + /** + * Return a new query for the given columns + * + * @param array $columns The desired columns, if null all columns will be queried + * + * @return RepositoryQuery + */ + public function select(array $columns = null) + { + $query = parent::select($columns); + $query->getQuery()->setBase($this->groupBaseDn); + if ($this->groupFilter) { + // TODO(jom): This should differentiate between groups and their memberships + $query->getQuery()->where(new Expression($this->groupFilter)); + } + + return $query; + } + + /** + * Initialize this repository's query columns + * + * @return array + * + * @throws ProgrammingError In case either $this->groupNameAttribute or $this->groupClass has not been set yet + */ + protected function initializeQueryColumns() + { + if ($this->groupClass === null) { + throw new ProgrammingError('It is required to set the objectClass where to look for groups first'); + } + if ($this->groupNameAttribute === null) { + throw new ProgrammingError('It is required to set a attribute name where to find a group\'s name first'); + } + + if ($this->ds->getCapabilities()->hasAdOid()) { + $createdAtAttribute = 'whenCreated'; + $lastModifiedAttribute = 'whenChanged'; + } else { + $createdAtAttribute = 'createTimestamp'; + $lastModifiedAttribute = 'modifyTimestamp'; + } + + // TODO(jom): Fetching memberships does not work currently, we'll need some aggregate functionality! + $columns = array( + 'group' => $this->groupNameAttribute, + 'group_name' => $this->groupNameAttribute, + 'user' => $this->groupMemberAttribute, + 'user_name' => $this->groupMemberAttribute, + 'created_at' => $createdAtAttribute, + 'last_modified' => $lastModifiedAttribute + ); + return array('group' => $columns, 'group_membership' => $columns); + } + + /** + * Initialize this repository's conversion rules + * + * @return array + * + * @throws ProgrammingError In case $this->groupClass has not been set yet + */ + protected function initializeConversionRules() + { + if ($this->groupClass === null) { + throw new ProgrammingError('It is required to set the objectClass where to look for groups first'); + } + + return array( + $this->groupClass => array( + 'created_at' => 'generalized_time', + 'last_modified' => 'generalized_time' + ) + ); + } + + /** + * Validate that the requested table exists + * + * This will return $this->groupClass in case $table equals "group" or "group_membership". + * + * @param string $table The table to validate + * @param RepositoryQuery $query An optional query to pass as context + * (unused by the base implementation) + * + * @return string + * + * @throws ProgrammingError In case the given table does not exist + */ + public function requireTable($table, RepositoryQuery $query = null) + { + $table = parent::requireTable($table, $query); + if ($table === 'group' || $table === 'group_membership') { + $table = $this->groupClass; + } + + return $table; + } + + /** + * Return the groups the given user is a member of + * + * @param User $user + * + * @return array + */ + public function getMemberships(User $user) + { + $userQuery = $this->ds + ->select() + ->from($this->userClass) + ->where($this->userNameAttribute, $user->getUsername()) + ->setBase($this->userBaseDn) + ->setUsePagedResults(false); + if ($this->userFilter) { + $userQuery->where(new Expression($this->userFilter)); + } + + if (($userDn = $userQuery->fetchDn()) === null) { + return array(); + } + + $groupQuery = $this->ds + ->select() + ->from($this->groupClass, array($this->groupNameAttribute)) + ->where($this->groupMemberAttribute, $userDn) + ->setBase($this->groupBaseDn); + if ($this->groupFilter) { + $groupQuery->where(new Expression($this->groupFilter)); + } + + $groups = array(); + foreach ($groupQuery as $row) { + $groups[] = $row->{$this->groupNameAttribute}; + } + + return $groups; + } + + /** + * Apply the given configuration on this backend + * + * @param ConfigObject $config + * + * @return $this + * + * @throws ConfigurationError In case a linked user backend does not exist or is invalid + */ + public function setConfig(ConfigObject $config) + { + if ($config->backend === 'ldap') { + $defaults = $this->getOpenLdapDefaults(); + } elseif ($config->backend === 'msldap') { + $defaults = $this->getActiveDirectoryDefaults(); + } else { + $defaults = new ConfigObject(); + } + + if ($config->user_backend && $config->user_backend !== 'none') { + $userBackend = UserBackend::create($config->user_backend); + if (! $userBackend instanceof LdapUserBackend) { + throw new ConfigurationError('User backend "%s" is not of type LDAP', $config->user_backend); + } + + if ( + $this->ds->getHostname() !== $userBackend->getDataSource()->getHostname() + || $this->ds->getPort() !== $userBackend->getDataSource()->getPort() + ) { + // TODO(jom): Elaborate whether it makes sense to link directories on different hosts + throw new ConfigurationError( + 'It is required that a linked user backend refers to the ' + . 'same directory as it\'s user group backend counterpart' + ); + } + + $defaults->merge(array( + 'user_base_dn' => $userBackend->getBaseDn(), + 'user_class' => $userBackend->getUserClass(), + 'user_name_attribute' => $userBackend->getUserNameAttribute(), + 'user_filter' => $userBackend->getFilter() + )); + } + + return $this + ->setGroupBaseDn($config->base_dn) + ->setUserBaseDn($config->get('user_base_dn', $this->getGroupBaseDn())) + ->setGroupClass($config->get('group_class', $defaults->group_class)) + ->setUserClass($config->get('user_class', $defaults->user_class)) + ->setGroupNameAttribute($config->get('group_name_attribute', $defaults->group_name_attribute)) + ->setUserNameAttribute($config->get('user_name_attribute', $defaults->user_name_attribute)) + ->setGroupMemberAttribute($config->get('group_member_attribute', $defaults->group_member_attribute)) + ->setGroupFilter($config->filter) + ->setUserFilter($config->user_filter); + } + + /** + * Return the configuration defaults for an OpenLDAP environment + * + * @return ConfigObject + */ + public function getOpenLdapDefaults() + { + return new ConfigObject(array( + 'group_class' => 'group', + 'user_class' => 'inetOrgPerson', + 'group_name_attribute' => 'gid', + 'user_name_attribute' => 'uid', + 'group_member_attribute' => 'member' + )); + } + + /** + * Return the configuration defaults for an ActiveDirectory environment + * + * @return ConfigObject + */ + public function getActiveDirectoryDefaults() + { + return new ConfigObject(array( + 'group_class' => 'group', + 'user_class' => 'user', + 'group_name_attribute' => 'sAMAccountName', + 'user_name_attribute' => 'sAMAccountName', + 'group_member_attribute' => 'member' + )); + } +} diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php index dd4900ea8..978860a37 100644 --- a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php @@ -21,6 +21,8 @@ class UserGroupBackend */ protected static $defaultBackends = array( 'db', + 'ldap', + 'msldap', //'ini' ); @@ -156,6 +158,11 @@ class UserGroupBackend case 'ini': $backend = new IniUserGroupBackend($resource); break; + case 'ldap': + case 'msldap': + $backend = new LdapUserGroupBackend($resource); + $backend->setConfig($backendConfig); + break; } $backend->setName($name); diff --git a/library/Icinga/File/Ini/IniEditor.php b/library/Icinga/File/Ini/IniEditor.php index 1f8202d9d..00bea8df4 100644 --- a/library/Icinga/File/Ini/IniEditor.php +++ b/library/Icinga/File/Ini/IniEditor.php @@ -313,14 +313,19 @@ class IniEditor */ public function getText() { - $this->cleanUpWhitespaces(); - return rtrim(implode(PHP_EOL, $this->text)) . PHP_EOL; + $this->normalizeSectionSpacing(); + + // trim leading and trailing whitespaces from generated file + $txt = trim(implode(PHP_EOL, $this->text)) . PHP_EOL; + + // replace linebreaks, unless they preceed a comment or a section + return preg_replace("/\n[\n]*([^;\[])/", "\n$1", $txt); } /** - * Remove all unneeded line breaks between sections + * normalize section spacing according to the current settings */ - private function cleanUpWhitespaces() + private function normalizeSectionSpacing() { $i = count($this->text) - 1; for (; $i > 0; $i--) { @@ -328,24 +333,18 @@ class IniEditor if ($this->isSectionDeclaration($line) && $i > 0) { $i--; $line = $this->text[$i]; - /* - * Ignore comments that are glued to the section declaration - */ + // ignore comments that are glued to the section declaration while ($i > 0 && $this->isComment($line)) { $i--; $line = $this->text[$i]; } - /* - * Remove whitespaces between the sections - */ + // remove whitespaces between the sections while ($i > 0 && preg_match('/^\s*$/', $line) === 1) { $this->deleteLine($i); $i--; $line = $this->text[$i]; } - /* - * Refresh section separators - */ + // refresh section separators if ($i !== 0 && $this->sectionSeparators > 0) { $this->insertAtLine($i + 1, str_repeat(PHP_EOL, $this->sectionSeparators - 1)); } @@ -621,6 +620,6 @@ class IniEditor private function sanitize($value) { - return str_replace('\n', '', $value); + return str_replace("\n", '', $value); } } diff --git a/library/Icinga/File/Ini/IniWriter.php b/library/Icinga/File/Ini/IniWriter.php index e43c226aa..5b565a6a0 100644 --- a/library/Icinga/File/Ini/IniWriter.php +++ b/library/Icinga/File/Ini/IniWriter.php @@ -60,7 +60,7 @@ class IniWriter extends Zend_Config_Writer_FileAbstract { if (file_exists($this->_filename)) { $oldconfig = new Zend_Config_Ini($this->_filename); - $content = file_get_contents($this->_filename); + $content = trim(file_get_contents($this->_filename)); } else { $oldconfig = new Zend_Config(array()); $content = ''; diff --git a/library/Icinga/Repository/LdapRepository.php b/library/Icinga/Repository/LdapRepository.php new file mode 100644 index 000000000..7cf00ae66 --- /dev/null +++ b/library/Icinga/Repository/LdapRepository.php @@ -0,0 +1,66 @@ + + *
  • Attribute name normalization
  • + * + */ +abstract class LdapRepository extends Repository +{ + /** + * The datasource being used + * + * @var Connection + */ + protected $ds; + + /** + * Normed attribute names based on known LDAP environments + * + * @var array + */ + protected $normedAttributes = array( + 'uid' => 'uid', + 'gid' => 'gid', + 'user' => 'user', + 'group' => 'group', + 'member' => 'member', + 'inetorgperson' => 'inetOrgPerson', + 'samaccountname' => 'sAMAccountName' + ); + + /** + * Create a new LDAP repository object + * + * @param Connection $ds The data source to use + */ + public function __construct(Connection $ds) + { + parent::__construct($ds); + } + + /** + * Return the given attribute name normed to known LDAP enviroments, if possible + * + * @param string $name + * + * @return string + */ + protected function getNormedAttribute($name) + { + $loweredName = strtolower($name); + if (array_key_exists($loweredName, $this->normedAttributes)) { + return $this->normedAttributes[$loweredName]; + } + + return $name; + } +} \ No newline at end of file diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 803f4958f..98b3e0ab4 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -599,6 +599,37 @@ abstract class Repository implements Selectable return $value; } + /** + * Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation + * + * @param string|null $value + * + * @return int + */ + protected function retrieveGeneralizedTime($value) + { + if ($value === null) { + return $value; + } + + if ( + ($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false + ) { + return $dateTime->getTimeStamp(); + } else { + Logger::debug(sprintf( + 'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) in repository "%s".', + $value, + $this->getName() + )); + } + } + /** * Validate that the requested table exists * diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php index 48f7acdb7..08cb02388 100644 --- a/library/Icinga/Web/Form.php +++ b/library/Icinga/Web/Form.php @@ -855,8 +855,19 @@ class Form extends Zend_Form public function populate(array $defaults) { $this->create($defaults); + $this->preserveDefaults($this, $defaults); + return parent::populate($defaults); + } - foreach ($this->getElements() as $name => $_) { + /** + * Recurse the given form and unset all unchanged default values + * + * @param Zend_Form $form + * @param array $defaults + */ + protected function preserveDefaults(Zend_Form $form, array & $defaults) + { + foreach ($form->getElements() as $name => $_) { if ( array_key_exists($name, $defaults) && array_key_exists($name . static::DEFAULT_SUFFIX, $defaults) @@ -866,7 +877,9 @@ class Form extends Zend_Form } } - return parent::populate($defaults); + foreach ($form->getSubForms() as $_ => $subForm) { + $this->preserveDefaults($subForm, $defaults); + } } /** diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index 63d92fc9f..45dd2dd9c 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -279,6 +279,11 @@ class Menu implements RecursiveIterator 'priority' => 990, 'renderer' => 'ForeignMenuItemRenderer' )); + + $this->add(t('About'), array( + 'url' => 'about', + 'priority' => 1000 + )); } } diff --git a/library/Icinga/Web/View/helpers/url.php b/library/Icinga/Web/View/helpers/url.php index 39e8de951..06ace7f19 100644 --- a/library/Icinga/Web/View/helpers/url.php +++ b/library/Icinga/Web/View/helpers/url.php @@ -20,7 +20,12 @@ $this->addHelperFunction('url', function ($path = null, $params = null) { } else { $url = Url::fromPath($path); } + if ($params !== null) { + if ($url === $path) { + $url = clone $url; + } + $url->overwriteParams($params); } diff --git a/modules/doc/application/views/scripts/search/index.phtml b/modules/doc/application/views/scripts/search/index.phtml index 3311c47a1..c613f04df 100644 --- a/modules/doc/application/views/scripts/search/index.phtml +++ b/modules/doc/application/views/scripts/search/index.phtml @@ -1,8 +1,8 @@
    $search): ?> - isEmpty()): ?> -

    escape($title) ?>

    - - +

    escape($title) ?>

    + isEmpty() + ? $this->translate('No documentation found matching the filter') + : $search ?>
    diff --git a/modules/monitoring/application/views/scripts/timeline/index.phtml b/modules/monitoring/application/views/scripts/timeline/index.phtml index d0cc8d34c..209eb0f1b 100644 --- a/modules/monitoring/application/views/scripts/timeline/index.phtml +++ b/modules/monitoring/application/views/scripts/timeline/index.phtml @@ -132,8 +132,8 @@ $extrapolatedCircleWidth = $timeline->getExtrapolatedCircleWidth($timeInfo[1][$g