diff --git a/.puppet/modules/openldap/manifests/init.pp b/.puppet/modules/openldap/manifests/init.pp index a0480632e..ee62e9b64 100644 --- a/.puppet/modules/openldap/manifests/init.pp +++ b/.puppet/modules/openldap/manifests/init.pp @@ -14,11 +14,12 @@ # class openldap { - package { ['openldap-servers', 'openldap-clients']: + package { [ 'openldap-servers', 'openldap-clients', ]: ensure => latest, } service { 'slapd': + enable => true, ensure => running, require => Package['openldap-servers'], } diff --git a/.puppet/modules/pgsql/manifests/init.pp b/.puppet/modules/pgsql/manifests/init.pp index 4b48cf895..dfa105d65 100644 --- a/.puppet/modules/pgsql/manifests/init.pp +++ b/.puppet/modules/pgsql/manifests/init.pp @@ -1,7 +1,7 @@ # Class: pgsql # -# This class installs the postgresql server and client software. -# Further it configures pg_hba.conf to trus the local icinga user. +# This class installs the PostgreSQL server and client software. +# Further it configures pg_hba.conf to trust the local icinga user. # # Parameters: # @@ -17,26 +17,25 @@ class pgsql { Exec { path => '/sbin:/bin:/usr/bin' } - package { [ - 'postgresql', 'postgresql-server' - ]: - ensure => latest, + package { [ 'postgresql', 'postgresql-server', ]: + ensure => latest, } exec { 'initdb': - creates => '/var/lib/pgsql/data/pg_xlog', command => 'service postgresql initdb', - require => Package['postgresql-server'] + creates => '/var/lib/pgsql/data/pg_xlog', + require => Package['postgresql-server'], } service { 'postgresql': + enable => true, ensure => running, - require => [Package['postgresql-server'], Exec['initdb']] + require => [ Package['postgresql-server'], Exec['initdb'], ] } file { '/var/lib/pgsql/data/pg_hba.conf': content => template('pgsql/pg_hba.conf.erb'), - require => [Package['postgresql-server'], Exec['initdb']], - notify => Service['postgresql'] + require => [ Package['postgresql-server'], Exec['initdb'], ], + notify => Service['postgresql'], } } diff --git a/RELEASE.md b/RELEASE.md index e8bcfeec0..da70d5bef 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -5,22 +5,47 @@ https://dev.icinga.org/projects/icingaweb2/roadmap # Release Workflow +## Authors + Update the [.mailmap](.mailmap) and [AUTHORS](AUTHORS) files: $ git log --use-mailmap | grep ^Author: | cut -f2- -d' ' | sort | uniq > AUTHORS -Update the version number in the [icingaweb2.spec] and [VERSION] files. +## Version + +Update the version number in the following files: + +* [icingaweb2.spec] (ensure that the revision is properly set) +* [VERSION] +* Application Version: [library/Icinga/Application/Version.php] +* Module Versions in modules/*/module.info + +Commands: + + VERSION=2.0.0 + + vim icingaweb2.spec + + echo "v$VERSION" > VERSION + + sed -i '' "s/const VERSION = '.*'/const VERSION = '$VERSION'/g" library/Icinga/Application/Version.php + + find . -type f -name '*.info' -exec sed -i '' "s/Version: .*/Version: $VERSION/g" {} \; + +## Changelog Update the [ChangeLog](ChangeLog) file using the changelog.py script. Changelog: - $ ./changelog.py --version 2.0.0-rc1 + $ ./changelog.py --version 2.0.0 Wordpress: - $ ./changelog.py --version 2.0.0-rc1 --html --links + $ ./changelog.py --version 2.0.0 --html --links + +## Git Tag Commit these changes to the "master" branch: diff --git a/application/VERSION b/application/VERSION index 519b667a7..f504e66fd 100644 --- a/application/VERSION +++ b/application/VERSION @@ -1 +1 @@ -$Format:%H%d %ci$ +$Format:%H %ci$ diff --git a/application/controllers/AboutController.php b/application/controllers/AboutController.php index fc1c78931..77d829679 100644 --- a/application/controllers/AboutController.php +++ b/application/controllers/AboutController.php @@ -3,13 +3,23 @@ namespace Icinga\Controllers; +use Icinga\Application\Icinga; use Icinga\Application\Version; -use Icinga\Web\Controller\ActionController; +use Icinga\Web\Controller; -class AboutController extends ActionController +class AboutController extends Controller { public function indexAction() { $this->view->version = Version::get(); + $this->view->modules = Icinga::app()->getModuleManager()->getLoadedModules(); + $this->view->tabs = $this->getTabs()->add( + 'about', + array( + 'label' => $this->translate('About'), + 'title' => $this->translate('About Icinga Web 2'), + 'url' => 'about' + ) + )->activate('about'); } } diff --git a/application/controllers/ErrorController.php b/application/controllers/ErrorController.php index 62198ec0c..ed35c3162 100644 --- a/application/controllers/ErrorController.php +++ b/application/controllers/ErrorController.php @@ -35,6 +35,10 @@ class ErrorController extends ActionController Logger::error($exception); Logger::error('Stacktrace: %s', $exception->getTraceAsString()); + if (! ($isAuthenticated = $this->Auth()->isAuthenticated())) { + $this->innerLayout = 'error'; + } + switch ($error->type) { case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE: case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER: @@ -45,11 +49,13 @@ class ErrorController extends ActionController $path = array_shift($path); $this->getResponse()->setHttpResponseCode(404); $this->view->message = $this->translate('Page not found.'); - if ($this->Auth()->isAuthenticated() && $modules->hasInstalled($path) && ! $modules->hasEnabled($path)) { - $this->view->message .= ' ' . sprintf( - $this->translate('Enabling the "%s" module might help!'), - $path - ); + if ($isAuthenticated) { + if ($modules->hasInstalled($path) && ! $modules->hasEnabled($path)) { + $this->view->message .= ' ' . sprintf( + $this->translate('Enabling the "%s" module might help!'), + $path + ); + } } break; @@ -93,5 +99,6 @@ class ErrorController extends ActionController } $this->view->request = $error->request; + $this->view->hideControls = ! $isAuthenticated; } } diff --git a/application/controllers/ListController.php b/application/controllers/ListController.php index b39811c0c..b9408f17c 100644 --- a/application/controllers/ListController.php +++ b/application/controllers/ListController.php @@ -10,6 +10,7 @@ use Icinga\Protocol\File\FileReader; use Icinga\Web\Controller; use Icinga\Web\Url; use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; use Icinga\Web\Widget\Tabextension\OutputFormat; /** @@ -30,7 +31,7 @@ class ListController extends Controller 'list/' . str_replace(' ', '', $action) ) - ))->extend(new OutputFormat())->extend(new DashboardAction())->activate($action); + ))->extend(new OutputFormat())->extend(new DashboardAction())->extend(new MenuAction())->activate($action); } /** diff --git a/application/controllers/NavigationController.php b/application/controllers/NavigationController.php index f5c1d785a..689a123cf 100644 --- a/application/controllers/NavigationController.php +++ b/application/controllers/NavigationController.php @@ -5,13 +5,13 @@ namespace Icinga\Controllers; use Exception; use Icinga\Application\Config; -use Icinga\Application\Icinga; use Icinga\Exception\NotFoundError; use Icinga\Data\DataArray\ArrayDatasource; use Icinga\Forms\ConfirmRemovalForm; use Icinga\Forms\Navigation\NavigationConfigForm; use Icinga\Web\Controller; use Icinga\Web\Form; +use Icinga\Web\Navigation\Navigation; use Icinga\Web\Notification; use Icinga\Web\Url; @@ -21,11 +21,11 @@ use Icinga\Web\Url; class NavigationController extends Controller { /** - * The default item types provided by Icinga Web 2 + * The global navigation item type configuration * * @var array */ - protected $defaultItemTypes; + protected $itemTypeConfig; /** * {@inheritdoc} @@ -33,11 +33,19 @@ class NavigationController extends Controller public function init() { parent::init(); + $this->itemTypeConfig = Navigation::getItemTypeConfiguration(); + } - $this->defaultItemTypes = array( - 'menu-item' => $this->translate('Menu Entry'), - 'dashlet' => 'Dashlet' - ); + /** + * Return the label for the given navigation item type + * + * @param string $type + * + * @return string $type if no label can be found + */ + protected function getItemLabel($type) + { + return isset($this->itemTypeConfig[$type]['label']) ? $this->itemTypeConfig[$type]['label'] : $type; } /** @@ -47,33 +55,71 @@ class NavigationController extends Controller */ protected function listItemTypes() { - $moduleManager = Icinga::app()->getModuleManager(); - - $types = $this->defaultItemTypes; - foreach ($moduleManager->getLoadedModules() as $module) { - if ($this->hasPermission($moduleManager::MODULE_PERMISSION_NS . $module->getName())) { - $moduleTypes = $module->getNavigationItems(); - if (! empty($moduleTypes)) { - $types = array_merge($types, $moduleTypes); - } - } + $types = array(); + foreach ($this->itemTypeConfig as $type => $options) { + $types[$type] = isset($options['label']) ? $options['label'] : $type; } return $types; } + /** + * Return all shared navigation item configurations + * + * @param string $owner A username if only items shared by a specific user are desired + * + * @return array + */ + protected function fetchSharedNavigationItemConfigs($owner = null) + { + $configs = array(); + foreach ($this->itemTypeConfig as $type => $_) { + $config = Config::navigation($type); + $config->getConfigObject()->setKeyColumn('name'); + $query = $config->select(); + if ($owner !== null) { + $query->where('owner', $owner); + } + + foreach ($query as $itemConfig) { + $configs[] = $itemConfig; + } + } + + return $configs; + } + + /** + * Return all user navigation item configurations + * + * @param string $username + * + * @return array + */ + protected function fetchUserNavigationItemConfigs($username) + { + $configs = array(); + foreach ($this->itemTypeConfig as $type => $_) { + $config = Config::navigation($type, $username); + $config->getConfigObject()->setKeyColumn('name'); + foreach ($config->select() as $itemConfig) { + $configs[] = $itemConfig; + } + } + + return $configs; + } + /** * Show the current user a list of his/her navigation items */ public function indexAction() { $user = $this->Auth()->getUser(); - $ds = new ArrayDatasource(array_merge( - Config::app('navigation')->select()->where('owner', $user->getUsername())->fetchAll(), - iterator_to_array($user->loadNavigationConfig()) + $this->fetchSharedNavigationItemConfigs($user->getUsername()), + $this->fetchUserNavigationItemConfigs($user->getUsername()) )); - $ds->setKeyColumn('name'); $query = $ds->select(); $this->view->types = $this->listItemTypes(); @@ -91,7 +137,7 @@ class NavigationController extends Controller array( 'type' => $this->translate('Type'), 'owner' => $this->translate('Shared'), - 'name' => $this->translate('Shared Navigation') + 'name' => $this->translate('Navigation') ), $query ); @@ -103,13 +149,11 @@ class NavigationController extends Controller public function sharedAction() { $this->assertPermission('config/application/navigation'); - $config = Config::app('navigation'); - $config->getConfigObject()->setKeyColumn('name'); - $query = $config->select(); + $ds = new ArrayDatasource($this->fetchSharedNavigationItemConfigs()); + $query = $ds->select(); $removeForm = new Form(); $removeForm->setUidDisabled(); - $removeForm->setAction(Url::fromPath('navigation/unshare')); $removeForm->addElement('hidden', 'name', array( 'decorators' => array('ViewHelper') )); @@ -156,11 +200,14 @@ class NavigationController extends Controller { $form = new NavigationConfigForm(); $form->setRedirectUrl('navigation'); + $form->setUser($this->Auth()->getUser()); $form->setItemTypes($this->listItemTypes()); $form->setTitle($this->translate('Create New Navigation Item')); $form->addDescription($this->translate('Create a new navigation item, such as a menu entry or dashlet.')); - $form->setUser($this->Auth()->getUser()); - $form->setShareConfig(Config::app('navigation')); + + // TODO: Fetch all "safe" parameters from the url and populate them + $form->populate(array('url' => rawurldecode($this->params->get('url', '')))); + $form->setOnSuccess(function (NavigationConfigForm $form) { $data = array_filter($form->getValues()); @@ -172,7 +219,7 @@ class NavigationController extends Controller } if ($form->save()) { - if (isset($data['type']) && $data['type'] === 'menu-item') { + if ($data['type'] === 'menu-item') { $form->getResponse()->setRerenderLayout(); } @@ -194,14 +241,22 @@ class NavigationController extends Controller public function editAction() { $itemName = $this->params->getRequired('name'); + $itemType = $this->params->getRequired('type'); $referrer = $this->params->get('referrer', 'index'); + $user = $this->Auth()->getUser(); + if ($user->can('config/application/navigation')) { + $itemOwner = $this->params->get('owner', $user->getUsername()); + } else { + $itemOwner = $user->getUsername(); + } + $form = new NavigationConfigForm(); + $form->setUser($user); + $form->setShareConfig(Config::navigation($itemType)); + $form->setUserConfig(Config::navigation($itemType, $itemOwner)); $form->setRedirectUrl($referrer === 'shared' ? 'navigation/shared' : 'navigation'); - $form->setItemTypes($this->listItemTypes()); - $form->setTitle(sprintf($this->translate('Edit Navigation Item %s'), $itemName)); - $form->setUser($this->Auth()->getUser()); - $form->setShareConfig(Config::app('navigation')); + $form->setTitle(sprintf($this->translate('Edit %s %s'), $this->getItemLabel($itemType), $itemName)); $form->setOnSuccess(function (NavigationConfigForm $form) use ($itemName) { $data = array_map( function ($v) { @@ -248,13 +303,17 @@ class NavigationController extends Controller public function removeAction() { $itemName = $this->params->getRequired('name'); + $itemType = $this->params->getRequired('type'); + $user = $this->Auth()->getUser(); $navigationConfigForm = new NavigationConfigForm(); - $navigationConfigForm->setUser($this->Auth()->getUser()); - $navigationConfigForm->setShareConfig(Config::app('navigation')); + $navigationConfigForm->setUser($user); + $navigationConfigForm->setShareConfig(Config::navigation($itemType)); + $navigationConfigForm->setUserConfig(Config::navigation($itemType, $user->getUsername())); + $form = new ConfirmRemovalForm(); $form->setRedirectUrl('navigation'); - $form->setTitle(sprintf($this->translate('Remove Navigation Item %s'), $itemName)); + $form->setTitle(sprintf($this->translate('Remove %s %s'), $this->getItemLabel($itemType), $itemName)); $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($itemName, $navigationConfigForm) { try { $itemConfig = $navigationConfigForm->delete($itemName); @@ -291,9 +350,14 @@ class NavigationController extends Controller $this->assertPermission('config/application/navigation'); $this->assertHttpMethod('POST'); + // TODO: I'd like these being form fields + $itemType = $this->params->getRequired('type'); + $itemOwner = $this->params->getRequired('owner'); + $navigationConfigForm = new NavigationConfigForm(); $navigationConfigForm->setUser($this->Auth()->getUser()); - $navigationConfigForm->setShareConfig(Config::app('navigation')); + $navigationConfigForm->setShareConfig(Config::navigation($itemType)); + $navigationConfigForm->setUserConfig(Config::navigation($itemType, $itemOwner)); $form = new Form(array( 'onSuccess' => function ($form) use ($navigationConfigForm) { diff --git a/application/forms/Config/Resource/DbResourceForm.php b/application/forms/Config/Resource/DbResourceForm.php index d79ad248b..c60ba65a9 100644 --- a/application/forms/Config/Resource/DbResourceForm.php +++ b/application/forms/Config/Resource/DbResourceForm.php @@ -3,8 +3,8 @@ namespace Icinga\Forms\Config\Resource; -use Icinga\Web\Form; use Icinga\Application\Platform; +use Icinga\Web\Form; /** * Form class for adding/modifying database resources @@ -43,12 +43,30 @@ class DbResourceForm extends Form $dbChoices['oci'] = 'Oracle (OCI8)'; } $offerPostgres = false; + $offerMysql = false; if (isset($formData['db'])) { if ($formData['db'] === 'pgsql') { $offerPostgres = true; + } elseif ($formData['db'] === 'mysql') { + $offerMysql = true; } - } elseif (key($dbChoices) === 'pgsql') { - $offerPostgres = true; + } else { + $dbChoice = key($dbChoices); + if ($dbChoice === 'pgsql') { + $offerPostgres = true; + } elseif ($dbChoices === 'mysql') { + $offerMysql = true; + } + } + $socketInfo = ''; + if ($offerPostgres) { + $socketInfo = $this->translate( + 'For using unix domain sockets, specify the path to the unix domain socket directory' + ); + } elseif ($offerMysql) { + $socketInfo = $this->translate( + 'For using unix domain sockets, specify localhost' + ); } $this->addElement( 'text', @@ -76,7 +94,8 @@ class DbResourceForm extends Form array ( 'required' => true, 'label' => $this->translate('Host'), - 'description' => $this->translate('The hostname of the database'), + 'description' => $this->translate('The hostname of the database') + . ($socketInfo ? '. ' . $socketInfo : ''), 'value' => 'localhost' ) ); @@ -119,6 +138,14 @@ class DbResourceForm extends Form 'description' => $this->translate('The password to use for authentication') ) ); + $this->addElement( + 'text', + 'charset', + array ( + 'description' => $this->translate('The character set for the database'), + 'label' => $this->translate('Character Set') + ) + ); $this->addElement( 'checkbox', 'persistent', diff --git a/application/forms/Navigation/NavigationConfigForm.php b/application/forms/Navigation/NavigationConfigForm.php index ba1f20d30..94ff860ae 100644 --- a/application/forms/Navigation/NavigationConfigForm.php +++ b/application/forms/Navigation/NavigationConfigForm.php @@ -123,12 +123,18 @@ class NavigationConfigForm extends ConfigForm /** * Return the user's navigation configuration * + * @param string $type + * * @return Config */ - public function getUserConfig() + public function getUserConfig($type = null) { if ($this->userConfig === null) { - $this->setUserConfig($this->getUser()->loadNavigationConfig()); + if ($type === null) { + throw new ProgrammingError('You need to pass a type if no user configuration is set'); + } + + $this->setUserConfig(Config::navigation($type, $this->getUser()->getUsername())); } return $this->userConfig; @@ -151,10 +157,20 @@ class NavigationConfigForm extends ConfigForm /** * Return the shared navigation configuration * + * @param string $type + * * @return Config */ - public function getShareConfig() + public function getShareConfig($type = null) { + if ($this->shareConfig === null) { + if ($type === null) { + throw new ProgrammingError('You need to pass a type if no share configuration is set'); + } + + $this->setShareConfig(Config::navigation($type)); + } + return $this->shareConfig; } @@ -194,10 +210,9 @@ class NavigationConfigForm extends ConfigForm $children = $this->itemToLoad ? $this->getFlattenedChildren($this->itemToLoad) : array(); $names = array(); - foreach ($this->getShareConfig() as $sectionName => $sectionConfig) { + foreach ($this->getShareConfig($type) as $sectionName => $sectionConfig) { if ( $sectionName !== $this->itemToLoad - && $sectionConfig->type === $type && $sectionConfig->owner === ($owner ?: $this->getUser()->getUsername()) && !in_array($sectionName, $children, true) ) { @@ -205,10 +220,9 @@ class NavigationConfigForm extends ConfigForm } } - foreach ($this->getUserConfig() as $sectionName => $sectionConfig) { + foreach ($this->getUserConfig($type) as $sectionName => $sectionConfig) { if ( $sectionName !== $this->itemToLoad - && $sectionConfig->type === $type && !in_array($sectionName, $children, true) ) { $names[] = $sectionName; @@ -271,29 +285,31 @@ class NavigationConfigForm extends ConfigForm * * @return $this * - * @throws InvalidArgumentException In case $data does not contain a navigation item name + * @throws InvalidArgumentException In case $data does not contain a navigation item name or type * @throws IcingaException In case a navigation item with the same name already exists */ public function add(array $data) { if (! isset($data['name'])) { throw new InvalidArgumentException('Key \'name\' missing'); + } elseif (! isset($data['type'])) { + throw new InvalidArgumentException('Key \'type\' missing'); } $shared = false; - $config = $this->getUserConfig(); + $config = $this->getUserConfig($data['type']); if ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) { if ($this->getUser()->can('application/share/navigation')) { $data['owner'] = $this->getUser()->getUsername(); - $config = $this->getShareConfig(); + $config = $this->getShareConfig($data['type']); $shared = true; } else { unset($data['users']); unset($data['groups']); } - } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'])) { + } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'], $data['type'])) { $data['owner'] = $this->getUser()->getUsername(); - $config = $this->getShareConfig(); + $config = $this->getShareConfig($data['type']); $shared = true; } @@ -301,9 +317,9 @@ class NavigationConfigForm extends ConfigForm $exists = $config->hasSection($itemName); if (! $exists) { if ($shared) { - $exists = $this->getUserConfig()->hasSection($itemName); + $exists = $this->getUserConfig($data['type'])->hasSection($itemName); } else { - $exists = (bool) $this->getShareConfig() + $exists = (bool) $this->getShareConfig($data['type']) ->select() ->where('name', $itemName) ->where('owner', $this->getUser()->getUsername()) @@ -385,8 +401,7 @@ class NavigationConfigForm extends ConfigForm if ($ownerName === $this->getUser()->getUsername()) { $exists = $this->getUserConfig()->hasSection($name); } else { - $owner = new User($ownerName); - $exists = $owner->loadNavigationConfig()->hasSection($name); + $exists = Config::navigation($itemConfig->type, $ownerName)->hasSection($name); } } else { $exists = (bool) $this->getShareConfig() @@ -521,8 +536,7 @@ class NavigationConfigForm extends ConfigForm if (! $itemConfig->owner || $itemConfig->owner === $this->getUser()->getUsername()) { $config = $this->getUserConfig(); } else { - $owner = new User($itemConfig->owner); - $config = $owner->loadNavigationConfig(); + $config = Config::navigation($itemConfig->type, $itemConfig->owner); } foreach ($children as $child) { @@ -549,6 +563,13 @@ class NavigationConfigForm extends ConfigForm $shared = false; $itemTypes = $this->getItemTypes(); $itemType = isset($formData['type']) ? $formData['type'] : key($itemTypes); + if ($itemType === null) { + throw new ProgrammingError( + 'This should actually not happen. Create a bug report at dev.icinga.org' + . ' or remove this assertion if you know what you\'re doing' + ); + } + $itemForm = $this->getItemForm($itemType); $this->addElement( @@ -606,17 +627,27 @@ class NavigationConfigForm extends ConfigForm } } - $this->addElement( - 'select', - 'type', - array( - 'required' => true, - 'autosubmit' => true, - 'label' => $this->translate('Type'), - 'description' => $this->translate('The type of this navigation item'), - 'multiOptions' => $itemTypes - ) - ); + if (empty($itemTypes) || count($itemTypes) === 1) { + $this->addElement( + 'hidden', + 'type', + array( + 'value' => $itemType + ) + ); + } else { + $this->addElement( + 'select', + 'type', + array( + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('Type'), + 'description' => $this->translate('The type of this navigation item'), + 'multiOptions' => $itemTypes + ) + ); + } if (! $shared && $itemForm->requiresParentSelection()) { if ($this->itemToLoad && $this->hasBeenShared($this->itemToLoad)) { @@ -767,12 +798,13 @@ class NavigationConfigForm extends ConfigForm * Return whether the given navigation item has been shared * * @param string $name + * @param string $type * * @return bool */ - protected function hasBeenShared($name) + protected function hasBeenShared($name, $type = null) { - return $this->getConfigForItem($name) === $this->getShareConfig(); + return $this->getShareConfig($type) === $this->getConfigForItem($name); } /** diff --git a/application/forms/Navigation/NavigationItemForm.php b/application/forms/Navigation/NavigationItemForm.php index 0011b044c..cd6eca70d 100644 --- a/application/forms/Navigation/NavigationItemForm.php +++ b/application/forms/Navigation/NavigationItemForm.php @@ -4,6 +4,7 @@ namespace Icinga\Forms\Navigation; use Icinga\Web\Form; +use Icinga\Web\Url; class NavigationItemForm extends Form { @@ -71,4 +72,20 @@ class NavigationItemForm extends Form ) ); } + + /** + * {@inheritdoc} + */ + public function getValues($suppressArrayNotation = false) + { + $values = parent::getValues($suppressArrayNotation); + if (isset($values['url']) && $values['url']) { + $url = Url::fromPath($values['url']); + if (! $url->isExternal() && ($relativePath = $url->getRelativeUrl())) { + $values['url'] = $relativePath; + } + } + + return $values; + } } diff --git a/application/layouts/scripts/error.phtml b/application/layouts/scripts/error.phtml new file mode 100644 index 000000000..c0ffd3394 --- /dev/null +++ b/application/layouts/scripts/error.phtml @@ -0,0 +1,8 @@ + + diff --git a/application/views/scripts/about/index.phtml b/application/views/scripts/about/index.phtml index e270a8eb9..2ffc47e76 100644 --- a/application/views/scripts/about/index.phtml +++ b/application/views/scripts/about/index.phtml @@ -1,25 +1,128 @@ -
-

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) - ) . '

'; - ?> +
+ +
+
+ img( + 'img/logo_icinga_big_dark.png', + null, + array( + 'width' => 400, + 'class' => 'about-logo' + ) + ); ?> +

+ + translate('Version'); ?>: escape($version['appVersion']); ?> + + +
+ translate('Git commit ID'); ?>: escape($version['gitCommitID']); ?> + + +
+ translate('Git commit date'); ?>: escape($version['gitCommitDate']); ?> + +

+

+ translate('Copyright'); ?>: © 2013- qlink( + $this->translate('The Icinga Project'), + 'https://www.icinga.org', + null, + array( + 'target' => '_blank' + ) + ); ?> +
+ translate('License'); ?>: GNU GPL v2+ +

+

+ qlink( + null, + 'https://www.twitter.com/icinga', + null, + array( + 'target' => '_blank', + 'icon' => 'twitter', + 'title' => $this->translate('Icinga on Twitter') + ) + ); ?> qlink( + null, + 'https://www.facebook.com/icinga', + null, + array( + 'target' => '_blank', + 'icon' => 'facebook-squared', + 'title' => $this->translate('Icinga on Facebook') + ) + ); ?> +

+
+
qlink( + null, + 'https://dev.icinga.org/projects/icingaweb2', + null, + array( + 'target' => '_blank', + 'img' => 'img/bugreport.png', + 'title' => $this->translate('Report a bug') + ) + ); ?> qlink( + null, + 'https://www.icinga.org/services/support', + null, + array( + 'target' => '_blank', + 'img' => 'img/support.png', + 'title' => $this->translate('Support / Mailinglists') + ) + ); ?>
+
qlink( + null, + 'https://wiki.icinga.org', + null, + array( + 'target' => '_blank', + 'img' => 'img/wiki.png', + 'title' => $this->translate('Icinga Wiki') + ) + ); ?> qlink( + null, + 'https://docs.icinga.org/', + null, + array( + 'target' => '_blank', + 'img' => 'img/docs.png', + 'title' => $this->translate('Icinga Documentation') + ) + ); ?>
+
+

translate('Loaded modules') ?>

+ + + + + + + + + + + + + + +
translate('Name') ?>translate('Version') ?>
+ hasPermission('config/modules')): ?> + qlink( + $module->getName(), + 'config/module/', + array('name' => $module->getName()), + array('title' => sprintf($this->translate('Show the overview of the %s module'), $module->getName())) + ); ?> + + escape($module->getName()); ?> + + + escape($module->getVersion()); ?> +
diff --git a/application/views/scripts/error/error.phtml b/application/views/scripts/error/error.phtml index 5b3480922..67429573c 100644 --- a/application/views/scripts/error/error.phtml +++ b/application/views/scripts/error/error.phtml @@ -1,10 +1,12 @@ +
-tabs->showOnlyCloseButton() ?> + showOnlyCloseButton(); ?>
-
-

escape($message)) ?>

- -
-
escape($stackTrace) ?>
-
+
+

escape($message)); ?>

+ +
+
escape($stackTrace) ?>
+ +
\ No newline at end of file diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml index 725f22a27..a7c3abed7 100644 --- a/application/views/scripts/group/show.phtml +++ b/application/views/scripts/group/show.phtml @@ -2,6 +2,7 @@ use Icinga\Data\Extensible; use Icinga\Data\Updatable; +use Icinga\Data\Selectable; $extensible = $this->hasPermission('config/authentication/groups/add') && $backend instanceof Extensible; @@ -67,7 +68,22 @@ foreach ($members as $member): ?> - escape($member->user_name); ?> + + hasPermission('config/authentication/users/show') + && ($userBackend = $backend->getUserBackend()) !== null + && $userBackend instanceof Selectable + ): ?> + qlink($member->user_name, 'user/show', array( + 'backend' => $userBackend->getName(), + 'user' => $member->user_name + ), array( + 'title' => sprintf($this->translate('Show detailed information about %s'), $member->user_name) + )); ?> + + escape($member->user_name); ?> + + getElement('user_name')->setValue($member->user_name); echo $removeForm; ?> diff --git a/application/views/scripts/navigation/index.phtml b/application/views/scripts/navigation/index.phtml index 7c81ba5a8..2f77d0995 100644 --- a/application/views/scripts/navigation/index.phtml +++ b/application/views/scripts/navigation/index.phtml @@ -22,14 +22,17 @@ translate('Remove'); ?> - $item): ?> + qlink( - $name, + $item->name, 'navigation/edit', - array('name' => $name), array( - 'title' => sprintf($this->translate('Edit navigation item %s'), $name) + 'name' => $item->name, + 'type' => $item->type + ), + array( + 'title' => sprintf($this->translate('Edit navigation item %s'), $item->name) ) ); ?> type && isset($types[$item->type]) @@ -39,10 +42,13 @@ qlink( '', 'navigation/remove', - array('name' => $name), + array( + 'name' => $item->name, + 'type' => $item->type + ), array( 'icon' => 'trash', - 'title' => sprintf($this->translate('Remove navigation item %s'), $name) + 'title' => sprintf($this->translate('Remove navigation item %s'), $item->name) ) ); ?> diff --git a/application/views/scripts/navigation/shared.phtml b/application/views/scripts/navigation/shared.phtml index 939249f84..5d0a3107f 100644 --- a/application/views/scripts/navigation/shared.phtml +++ b/application/views/scripts/navigation/shared.phtml @@ -1,4 +1,8 @@ -compact): ?> +compact): ?>
tabs; ?> sortBox; ?> @@ -19,17 +23,19 @@ translate('Unshare'); ?> - $item): ?> + qlink( - $name, + $item->name, 'navigation/edit', array( - 'name' => $name, + 'name' => $item->name, + 'type' => $item->type, + 'owner' => $item->owner, 'referrer' => 'shared' ), array( - 'title' => sprintf($this->translate('Edit shared navigation item %s'), $name) + 'title' => sprintf($this->translate('Edit shared navigation item %s'), $item->name) ) ); ?> type && isset($types[$item->type]) @@ -48,7 +54,12 @@ ) ); ?> - setDefault('name', $name); ?> + setDefault('name', $item->name) + ->setAction(Url::fromPath( + 'navigation/unshare', + array('type' => $item->type, 'owner' => $item->owner) + )); ?> diff --git a/doc/resources.md b/doc/resources.md index a2bfb66af..6b43ca3d3 100644 --- a/doc/resources.md +++ b/doc/resources.md @@ -19,21 +19,38 @@ to handle authentication and authorization, monitoring data or user preferences. Directive | Description ----------------|------------ **type** | `db` -**db** | Database management system. Either `mysql` or `pgsql`. -**host** | Connect to the database server on the given host. -**port** | Port number to use for the connection. +**db** | Database management system. In most cases `mysql` or `pgsql`. +**host** | Connect to the database server on the given host. For using unix domain sockets, specify `localhost` for MySQL and the path to the unix domain socket directory for PostgreSQL. +**port** | Port number to use. Mandatory for connections to a PostgreSQL database. **username** | The username to use when connecting to the server. **password** | The password to use when connecting to the server. **dbname** | The database to use. **Example:** -``` -[icingaweb] +```` +[icingaweb-mysql-tcp] +type = db +db = mysql +host = 127.0.0.1 +port = 3306 +username = icingaweb +password = icingaweb +dbname = icingaweb + +[icingaweb-mysql-socket] type = db db = mysql host = localhost -port = 3306 +username = icingaweb +password = icingaweb +dbname = icingaweb + +[icingaweb-pgsql-socket] +type = db +db = pgsql +host = /var/run/postgresql +port = 5432 username = icingaweb password = icingaweb dbname = icingaweb diff --git a/library/Icinga/Application/Config.php b/library/Icinga/Application/Config.php index bfe2ddd60..d25ec748c 100644 --- a/library/Icinga/Application/Config.php +++ b/library/Icinga/Application/Config.php @@ -13,7 +13,9 @@ use Icinga\Data\Selectable; use Icinga\Data\SimpleQuery; use Icinga\File\Ini\IniWriter; use Icinga\File\Ini\IniParser; +use Icinga\Exception\IcingaException; use Icinga\Exception\NotReadableError; +use Icinga\Web\Navigation\Navigation; /** * Container for INI like configuration and global registry of application and module related configuration. @@ -41,6 +43,13 @@ class Config implements Countable, Iterator, Selectable */ protected static $modules = array(); + /** + * Navigation config instances per type + * + * @var array + */ + protected static $navigation = array(); + /** * The internal ConfigObject * @@ -416,6 +425,60 @@ class Config implements Countable, Iterator, Selectable return $moduleConfigs[$configname]; } + /** + * Retrieve a navigation config + * + * @param string $type The type identifier of the navigation item for which to return its config + * @param string $username A user's name or null if the shared config is desired + * @param bool $fromDisk If true, the configuration will be read from disk + * + * @return Config The requested configuration + */ + public static function navigation($type, $username = null, $fromDisk = false) + { + if (! isset(self::$navigation[$type])) { + self::$navigation[$type] = array(); + } + + $branch = $username ?: 'shared'; + $typeConfigs = self::$navigation[$type]; + if (! isset($typeConfigs[$branch]) || $fromDisk) { + $typeConfigs[$branch] = static::fromIni(static::getNavigationConfigPath($type, $username)); + } + + return $typeConfigs[$branch]; + } + + /** + * Return the path to the configuration file for the given navigation item type and user + * + * @param string $type + * @param string $username + * + * @return string + * + * @throws IcingaException In case the given type is unknown + */ + protected static function getNavigationConfigPath($type, $username = null) + { + $itemTypeConfig = Navigation::getItemTypeConfiguration(); + if (! isset($itemTypeConfig[$type])) { + throw new IcingaException('Invalid navigation item type %s provided', $type); + } + + if (isset($itemTypeConfig[$type]['config'])) { + $filename = $itemTypeConfig[$type]['config'] . '.ini'; + } else { + $filename = $type . 's.ini'; + } + + return static::resolvePath( + ($username ? 'preferences' . DIRECTORY_SEPARATOR . $username : 'navigation') + . DIRECTORY_SEPARATOR + . $filename + ); + } + /** * Return this config rendered as a INI structured string * diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index ebbfc3d98..842a581a9 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -1014,16 +1014,21 @@ class Module } /** - * Provide a new type of configurable navigation item with a optional label + * Provide a new type of configurable navigation item with a optional label and config filename * * @param string $type * @param string $label + * @param string $config * * @return $this */ - protected function provideNavigationItem($type, $label = null) + protected function provideNavigationItem($type, $label = null, $config = null) { - $this->navigationItems[$type] = $label ?: $type; + $this->navigationItems[$type] = array( + 'label' => $label, + 'config' => $config + ); + return $this; } diff --git a/library/Icinga/Application/Version.php b/library/Icinga/Application/Version.php index c06eca2c2..e110031ea 100644 --- a/library/Icinga/Application/Version.php +++ b/library/Icinga/Application/Version.php @@ -8,33 +8,41 @@ namespace Icinga\Application; */ class Version { + const VERSION = '2.0.0-rc1'; + /** * Get the version of this instance of Icinga Web 2 * - * @return array|false array on success, false otherwise + * @return array */ public static function get() { - if (false === ($appVersion = @file_get_contents( - Icinga::app()->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]); + $version = array('appVersion' => self::VERSION); + if (false !== ($appVersion = @file_get_contents(Icinga::app()->getApplicationDir('VERSION')))) { + $matches = array(); + if (@preg_match('/^(?P\w+) (?P\S+)/', $appVersion, $matches)) { + return array_merge($version, $matches); } } - return $matches; + + $gitDir = Icinga::app()->getBaseDir('.git'); + $gitHead = @file_get_contents($gitDir . DIRECTORY_SEPARATOR . 'HEAD'); + if (false !== $gitHead) { + $matches = array(); + if (@preg_match('/(?[0-9a-f]+)$/ms', $gitCommitID, $matches)) { + return array_merge($version, $matches); + } + } + } + + return $version; } } diff --git a/library/Icinga/Application/Web.php b/library/Icinga/Application/Web.php index f91edc1d8..bb316a537 100644 --- a/library/Icinga/Application/Web.php +++ b/library/Icinga/Application/Web.php @@ -179,12 +179,11 @@ class Web extends EmbeddedWeb */ public function getSharedNavigation($type) { - $config = Config::app('navigation')->getConfigObject(); - $config->setKeyColumn('name'); + $config = Config::navigation($type === 'dashboard-pane' ? 'dashlet' : $type); if ($type === 'dashboard-pane') { $panes = array(); - foreach ($config->select()->where('type', 'dashlet') as $dashletName => $dashletConfig) { + foreach ($config as $dashletName => $dashletConfig) { if ($this->hasAccessToSharedNavigationItem($dashletConfig)) { // TODO: Throw ConfigurationError if pane or url is missing $panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url; @@ -203,7 +202,7 @@ class Web extends EmbeddedWeb } } else { $items = array(); - foreach ($config->select()->where('type', $type) as $name => $typeConfig) { + foreach ($config as $name => $typeConfig) { if ($this->hasAccessToSharedNavigationItem($typeConfig)) { $items[$name] = $typeConfig; } diff --git a/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php index 1d29e018d..2a737d535 100644 --- a/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php @@ -12,10 +12,16 @@ use Icinga\Protocol\Ldap\Expression; use Icinga\Repository\LdapRepository; use Icinga\Repository\RepositoryQuery; use Icinga\User; -use Icinga\Application\Logger; -class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBackendInterface +class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInterface { + /** + * The user backend being associated with this user group backend + * + * @var LdapUserBackend + */ + protected $userBackend; + /** * The base DN to use for a user query * @@ -105,84 +111,26 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken ); /** - * Normed attribute names based on known LDAP environments + * Set the user backend to be associated with this user group backend * - * @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 + * @param LdapUserBackend $backend * * @return $this */ - public function setName($name) + public function setUserBackend(LdapUserBackend $backend) { - $this->name = $name; + $this->userBackend = $backend; return $this; } /** - * Return this repository's name + * Return the user backend being associated with this user group backend * - * In case no name has been explicitly set yet, the class name is returned. - * - * @return string + * @return LdapUserBackend */ - public function getName() + public function getUserBackend() { - return $this->name; + return $this->userBackend; } /** @@ -453,7 +401,6 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken $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, @@ -492,13 +439,37 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken if ($this->groupClass === null) { throw new ProgrammingError('It is required to set the objectClass where to look for groups first'); } + if ($this->groupMemberAttribute === null) { + throw new ProgrammingError('It is required to set a attribute name where to find a group\'s members first'); + } - return array( + $rules = array( $this->groupClass => array( 'created_at' => 'generalized_time', 'last_modified' => 'generalized_time' ) ); + if (! $this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) { + $rules[$this->groupClass][] = 'user_name'; + } + + return $rules; + } + + /** + * Return the uid for the given distinguished name + * + * @param string $username + * + * @param string + */ + protected function retrieveUserName($dn) + { + return $this->ds + ->select() + ->from('*', array($this->userNameAttribute)) + ->setBase($dn) + ->fetchOne(); } /** @@ -524,6 +495,27 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken return $table; } + /** + * Validate that the given column is a valid query target and return it or the actual name if it's an alias + * + * @param string $table The table where to look for the column or alias + * @param string $name The name or alias of the column to validate + * @param RepositoryQuery $query An optional query to pass as context + * + * @return string The given column's name + * + * @throws QueryException In case the given column is not a valid query column + */ + public function requireQueryColumn($table, $name, RepositoryQuery $query = null) + { + $column = parent::requireQueryColumn($table, $name, $query); + if ($name === 'user_name' && $query !== null) { + $query->getQuery()->setUnfoldAttribute('user_name'); + } + + return $column; + } + /** * Return the groups the given user is a member of * @@ -533,43 +525,37 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken */ public function getMemberships(User $user) { - if ($this->groupClass === 'posixGroup') { - // Posix group only uses simple user name - $userDn = $user->getUsername(); - } else { - // LDAP groups use the complete DN - if (($userDn = $user->getAdditional('ldap_dn')) === null) { - $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 ($this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) { + $queryValue = $user->getUsername(); + } elseif (($queryValue = $user->getAdditional('ldap_dn')) === null) { + $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(); - } + if (($queryValue = $userQuery->fetchDn()) === null) { + return array(); } } $groupQuery = $this->ds ->select() ->from($this->groupClass, array($this->groupNameAttribute)) - ->where($this->groupMemberAttribute, $userDn) + ->where($this->groupMemberAttribute, $queryValue) ->setBase($this->groupBaseDn); if ($this->groupFilter) { $groupQuery->where(new Expression($this->groupFilter)); } - Logger::debug('Fetching groups for user %s using filter %s.', $user->getUsername(), $groupQuery->__toString()); $groups = array(); foreach ($groupQuery as $row) { $groups[] = $row->{$this->groupNameAttribute}; } - Logger::debug('Fetched %d groups: %s.', count($groups), join(', ', $groups)); return $groups; } @@ -610,6 +596,7 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken ); } + $this->setUserBackend($userBackend); $defaults->merge(array( 'user_base_dn' => $userBackend->getBaseDn(), 'user_class' => $userBackend->getUserClass(), @@ -661,4 +648,4 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken 'group_member_attribute' => 'member' )); } -} +} \ No newline at end of file diff --git a/library/Icinga/Data/DataArray/ArrayDatasource.php b/library/Icinga/Data/DataArray/ArrayDatasource.php index ef5b4e3ed..a91c1ceff 100644 --- a/library/Icinga/Data/DataArray/ArrayDatasource.php +++ b/library/Icinga/Data/DataArray/ArrayDatasource.php @@ -80,7 +80,7 @@ class ArrayDatasource implements Selectable */ public function select() { - return new SimpleQuery($this); + return new SimpleQuery(clone $this); } /** diff --git a/library/Icinga/Protocol/Ldap/LdapConnection.php b/library/Icinga/Protocol/Ldap/LdapConnection.php index ca83c4ff7..3c5a3e276 100644 --- a/library/Icinga/Protocol/Ldap/LdapConnection.php +++ b/library/Icinga/Protocol/Ldap/LdapConnection.php @@ -358,9 +358,25 @@ class LdapConnection implements Selectable, Inspectable */ public function count(LdapQuery $query) { - $ds = $this->getConnection(); $this->bind(); + if (($unfoldAttribute = $query->getUnfoldAttribute()) !== null) { + $desiredColumns = $query->getColumns(); + if (isset($desiredColumns[$unfoldAttribute])) { + $fields = array($unfoldAttribute => $desiredColumns[$unfoldAttribute]); + } elseif (in_array($unfoldAttribute, $desiredColumns, true)) { + $fields = array($unfoldAttribute); + } else { + throw new ProgrammingError( + 'The attribute used to unfold a query\'s result must be selected' + ); + } + + $res = $this->runQuery($query, $fields); + return count($res); + } + + $ds = $this->getConnection(); $results = @ldap_search( $ds, $query->getBase() ?: $this->getDn(), @@ -658,7 +674,7 @@ class LdapConnection implements Selectable, Inspectable protected function runQuery(LdapQuery $query, array $fields = null) { $limit = $query->getLimit(); - $offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; + $offset = $query->hasOffset() ? $query->getOffset() : 0; if ($fields === null) { $fields = $query->getColumns(); @@ -711,13 +727,41 @@ class LdapConnection implements Selectable, Inspectable $count = 0; $entries = array(); $entry = ldap_first_entry($ds, $results); + $unfoldAttribute = $query->getUnfoldAttribute(); do { - $count += 1; - if (! $serverSorting || $offset === 0 || $offset < $count) { - $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes( + if ($unfoldAttribute) { + $rows = $this->cleanupAttributes( ldap_get_attributes($ds, $entry), - array_flip($fields) + array_flip($fields), + $unfoldAttribute ); + + if (is_array($rows)) { + // TODO: Register the DN the same way as a section name in the ArrayDatasource! + foreach ($rows as $row) { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[] = $row; + } + + if ($serverSorting && $limit > 0 && $limit === count($entries)) { + break; + } + } + } else { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[ldap_get_dn($ds, $entry)] = $rows; + } + } + } else { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes( + ldap_get_attributes($ds, $entry), + array_flip($fields) + ); + } } } while ((! $serverSorting || $limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($ds, $entry)) @@ -754,7 +798,7 @@ class LdapConnection implements Selectable, Inspectable } $limit = $query->getLimit(); - $offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; + $offset = $query->hasOffset() ? $query->getOffset() : 0; $queryString = (string) $query; $base = $query->getBase() ?: $this->rootDn; @@ -776,6 +820,7 @@ class LdapConnection implements Selectable, Inspectable $count = 0; $cookie = ''; $entries = array(); + $unfoldAttribute = $query->getUnfoldAttribute(); do { // Do not request the pagination control as a critical extension, as we want the // server to return results even if the paged search request cannot be satisfied @@ -826,12 +871,39 @@ class LdapConnection implements Selectable, Inspectable $entry = ldap_first_entry($ds, $results); do { - $count += 1; - if (! $serverSorting || $offset === 0 || $offset < $count) { - $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes( + if ($unfoldAttribute) { + $rows = $this->cleanupAttributes( ldap_get_attributes($ds, $entry), - array_flip($fields) + array_flip($fields), + $unfoldAttribute ); + + if (is_array($rows)) { + // TODO: Register the DN the same way as a section name in the ArrayDatasource! + foreach ($rows as $row) { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[] = $row; + } + + if ($serverSorting && $limit > 0 && $limit === count($entries)) { + break; + } + } + } else { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[ldap_get_dn($ds, $entry)] = $rows; + } + } + } else { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes( + ldap_get_attributes($ds, $entry), + array_flip($fields) + ); + } } } while ( (! $serverSorting || $limit === 0 || $limit !== count($entries)) @@ -861,9 +933,6 @@ class LdapConnection implements Selectable, Inspectable // the server: https://www.ietf.org/rfc/rfc2696.txt ldap_control_paged_result($ds, 0, false, $cookie); ldap_search($ds, $base, $queryString); // Returns no entries, due to the page size - } else { - // Reset the paged search request so that subsequent requests succeed - ldap_control_paged_result($ds, 0); } if (! $serverSorting && $query->hasOrder()) { @@ -879,14 +948,16 @@ class LdapConnection implements Selectable, Inspectable /** * Clean up the given attributes and return them as simple object * - * Applies column aliases, aggregates multi-value attributes as array and sets null for each missing attribute. + * Applies column aliases, aggregates/unfolds multi-value attributes + * as array and sets null for each missing attribute. * * @param array $attributes * @param array $requestedFields + * @param string $unfoldAttribute * - * @return object + * @return object|array An array in case the object has been unfolded */ - public function cleanupAttributes($attributes, array $requestedFields) + public function cleanupAttributes($attributes, array $requestedFields, $unfoldAttribute = null) { // In case the result contains attributes with a differing case than the requested fields, it is // necessary to create another array to map attributes case insensitively to their requested counterparts. @@ -927,6 +998,24 @@ class LdapConnection implements Selectable, Inspectable } } + if ( + $unfoldAttribute !== null + && isset($cleanedAttributes[$unfoldAttribute]) + && is_array($cleanedAttributes[$unfoldAttribute]) + ) { + $values = $cleanedAttributes[$unfoldAttribute]; + unset($cleanedAttributes[$unfoldAttribute]); + $baseRow = (object) $cleanedAttributes; + $rows = array(); + foreach ($values as $value) { + $row = clone $baseRow; + $row->{$unfoldAttribute} = $value; + $rows[] = $row; + } + + return $rows; + } + return (object) $cleanedAttributes; } diff --git a/library/Icinga/Protocol/Ldap/LdapQuery.php b/library/Icinga/Protocol/Ldap/LdapQuery.php index 1af5467d7..6d76bb897 100644 --- a/library/Icinga/Protocol/Ldap/LdapQuery.php +++ b/library/Icinga/Protocol/Ldap/LdapQuery.php @@ -35,6 +35,13 @@ class LdapQuery extends SimpleQuery */ protected $usePagedResults; + /** + * The name of the attribute used to unfold the result + * + * @var string + */ + protected $unfoldAttribute; + /** * Initialize this query */ @@ -90,6 +97,29 @@ class LdapQuery extends SimpleQuery return $this->usePagedResults; } + /** + * Set the attribute to be used to unfold the result + * + * @param string $attributeName + * + * @return $this + */ + public function setUnfoldAttribute($attributeName) + { + $this->unfoldAttribute = $attributeName; + return $this; + } + + /** + * Return the attribute to use to unfold the result + * + * @return string + */ + public function getUnfoldAttribute() + { + return $this->unfoldAttribute; + } + /** * Choose an objectClass and the columns you are interested in * diff --git a/library/Icinga/Repository/LdapRepository.php b/library/Icinga/Repository/LdapRepository.php index ac210516d..5e79c41bb 100644 --- a/library/Icinga/Repository/LdapRepository.php +++ b/library/Icinga/Repository/LdapRepository.php @@ -28,13 +28,27 @@ abstract class LdapRepository extends Repository * @var array */ protected $normedAttributes = array( - 'uid' => 'uid', - 'gid' => 'gid', - 'user' => 'user', - 'group' => 'group', - 'member' => 'member', - 'inetorgperson' => 'inetOrgPerson', - 'samaccountname' => 'sAMAccountName' + 'uid' => 'uid', + 'gid' => 'gid', + 'user' => 'user', + 'group' => 'group', + 'member' => 'member', + 'memberuid' => 'memberUid', + 'posixgroup' => 'posixGroup', + 'uniquemember' => 'uniqueMember', + 'groupofnames' => 'groupOfNames', + 'inetorgperson' => 'inetOrgPerson', + 'samaccountname' => 'sAMAccountName', + 'groupofuniquenames' => 'groupOfUniqueNames' + ); + + /** + * Object attributes whose value is not distinguished name + * + * @var array + */ + protected $ambiguousAttributes = array( + 'posixGroup' => 'memberUid' ); /** @@ -63,4 +77,17 @@ abstract class LdapRepository extends Repository return $name; } -} \ No newline at end of file + + /** + * Return whether the given object attribute's value is not a distinguished name + * + * @param string $objectClass + * @param string $attributeName + * + * @return bool + */ + protected function isAmbiguous($objectClass, $attributeName) + { + return isset($this->ambiguousAttributes[$objectClass][$attributeName]); + } +} diff --git a/library/Icinga/User.php b/library/Icinga/User.php index 114e59755..626905e9c 100644 --- a/library/Icinga/User.php +++ b/library/Icinga/User.php @@ -479,22 +479,6 @@ class User return false; } - /** - * Load and return this user's navigation configuration - * - * @return Config - */ - public function loadNavigationConfig() - { - return Config::fromIni( - Config::resolvePath('preferences') - . DIRECTORY_SEPARATOR - . $this->getUsername() - . DIRECTORY_SEPARATOR - . 'navigation.ini' - ); - } - /** * Load and return this user's configured navigation of the given type * @@ -504,12 +488,11 @@ class User */ public function getNavigation($type) { - $config = $this->loadNavigationConfig(); - $config->getConfigObject()->setKeyColumn('name'); + $config = Config::navigation($type === 'dashboard-pane' ? 'dashlet' : $type, $this->getUsername()); if ($type === 'dashboard-pane') { $panes = array(); - foreach ($config->select()->where('type', 'dashlet') as $dashletName => $dashletConfig) { + foreach ($config as $dashletName => $dashletConfig) { // TODO: Throw ConfigurationError if pane or url is missing $panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url; } @@ -525,7 +508,7 @@ class User ); } } else { - $navigation = Navigation::fromConfig($config->select()->where('type', $type)); + $navigation = Navigation::fromConfig($config); } return $navigation; diff --git a/library/Icinga/Web/FileCache.php b/library/Icinga/Web/FileCache.php index 57c0c0964..208640868 100644 --- a/library/Icinga/Web/FileCache.php +++ b/library/Icinga/Web/FileCache.php @@ -1,5 +1,6 @@ merge(Icinga::app()->getSharedNavigation($type)); - - // User Preferences $user = Auth::getInstance()->getUser(); - $this->merge($user->getNavigation($type)); + if ($type !== 'dashboard-pane') { + // Shareables + $this->merge(Icinga::app()->getSharedNavigation($type)); + + // User Preferences + $this->merge($user->getNavigation($type)); + } // Modules $moduleManager = Icinga::app()->getModuleManager(); @@ -451,6 +453,39 @@ class Navigation implements ArrayAccess, Countable, IteratorAggregate return $this; } + /** + * Return the global navigation item type configuration + * + * @return array + */ + public static function getItemTypeConfiguration() + { + $defaultItemTypes = array( + 'menu-item' => array( + 'label' => t('Menu Entry'), + 'config' => 'menu' + )/*, // Disabled, until it is able to fully replace the old implementation + 'dashlet' => array( + 'label' => 'Dashlet', + 'config' => 'dashboard' + )*/ + ); + + $moduleItemTypes = array(); + $moduleManager = Icinga::app()->getModuleManager(); + foreach ($moduleManager->getLoadedModules() as $module) { + if (Auth::getInstance()->hasPermission($moduleManager::MODULE_PERMISSION_NS . $module->getName())) { + foreach ($module->getNavigationItems() as $type => $options) { + if (! isset($moduleItemTypes[$type])) { + $moduleItemTypes[$type] = $options; + } + } + } + } + + return array_merge($defaultItemTypes, $moduleItemTypes); + } + /** * Create and return a new set of navigation items for the given configuration * diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php index 832e61064..3b739c320 100644 --- a/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php +++ b/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php @@ -36,6 +36,13 @@ class NavigationItemRenderer */ protected $internalLinkTargets; + /** + * Whether to escape the label + * + * @var bool + */ + protected $escapeLabel; + /** * Create a new NavigationItemRenderer * @@ -126,6 +133,29 @@ class NavigationItemRenderer return $this->item; } + /** + * Set whether to escape the label + * + * @param bool $state + * + * @return $this + */ + public function setEscapeLabel($state = true) + { + $this->escapeLabel = (bool) $state; + return $this; + } + + /** + * Return whether to escape the label + * + * @return bool + */ + public function getEscapeLabel() + { + return $this->escapeLabel !== null ? $this->escapeLabel : true; + } + /** * Render the given navigation item as HTML anchor * @@ -144,7 +174,9 @@ class NavigationItemRenderer ); } - $label = $this->view()->escape($item->getLabel()); + $label = $this->getEscapeLabel() + ? $this->view()->escape($item->getLabel()) + : $item->getLabel(); if (($icon = $item->getIcon()) !== null) { $label = $this->view()->icon($icon) . $label; } diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php index d1aab6877..0b1eac56c 100644 --- a/library/Icinga/Web/StyleSheet.php +++ b/library/Icinga/Web/StyleSheet.php @@ -37,7 +37,9 @@ class StyleSheet 'css/icinga/selection-toolbar.less', 'css/icinga/login.less', 'css/icinga/controls.less', - 'css/icinga/dev.less' + 'css/icinga/dev.less', + 'css/icinga/logo.less', + 'css/icinga/about.less' ); public static function compileForPdf() diff --git a/library/Icinga/Web/Url.php b/library/Icinga/Web/Url.php index 9f31447ea..37791986c 100644 --- a/library/Icinga/Web/Url.php +++ b/library/Icinga/Web/Url.php @@ -75,7 +75,7 @@ class Url } $url = new Url(); - $url->setPath($request->getPathInfo()); + $url->setPath(ltrim($request->getPathInfo(), '/')); // $urlParams = UrlParams::fromQueryString($request->getQuery()); if (isset($_SERVER['QUERY_STRING'])) { @@ -159,16 +159,17 @@ class Url if (isset($urlParts['path'])) { $urlPath = $urlParts['path']; if ($urlPath && $urlPath[0] === '/') { - $baseUrl = ''; + if ($baseUrl) { + $urlPath = substr($urlPath, 1); + } elseif (strpos($urlPath, $request->getBaseUrl()) === 0) { + $baseUrl = $request->getBaseUrl(); + $urlPath = substr($urlPath, strlen($baseUrl) + 1); + } } elseif (! $baseUrl) { $baseUrl = $request->getBaseUrl(); } - if ($baseUrl && !$urlObject->isExternal() && strpos($urlPath, $baseUrl) === 0) { - $urlObject->setPath(substr($urlPath, strlen($baseUrl))); - } else { - $urlObject->setPath($urlPath); - } + $urlObject->setPath($urlPath); } elseif (! $baseUrl) { $baseUrl = $request->getBaseUrl(); } @@ -255,7 +256,7 @@ class Url */ public function setPath($path) { - $this->path = ltrim($path, '/'); + $this->path = $path; return $this; } diff --git a/library/Icinga/Web/View/helpers/url.php b/library/Icinga/Web/View/helpers/url.php index 06ace7f19..962f3cb6e 100644 --- a/library/Icinga/Web/View/helpers/url.php +++ b/library/Icinga/Web/View/helpers/url.php @@ -43,6 +43,11 @@ $this->addHelperFunction('qlink', function ($title, $url, $params = null, $prope $icon = $view->icon($properties['icon']); unset($properties['icon']); } + + if (array_key_exists('img', $properties)) { + $icon = $view->img($properties['img']); + unset($properties['img']); + } } return sprintf( diff --git a/library/Icinga/Web/Widget/Dashboard.php b/library/Icinga/Web/Widget/Dashboard.php index c69fb4477..4ff192122 100644 --- a/library/Icinga/Web/Widget/Dashboard.php +++ b/library/Icinga/Web/Widget/Dashboard.php @@ -85,6 +85,7 @@ class Dashboard extends AbstractWidget } $this->mergePanes($panes); + $this->loadUserDashboards(); return $this; } diff --git a/library/Icinga/Web/Widget/Tabextension/MenuAction.php b/library/Icinga/Web/Widget/Tabextension/MenuAction.php new file mode 100644 index 000000000..7291f7595 --- /dev/null +++ b/library/Icinga/Web/Widget/Tabextension/MenuAction.php @@ -0,0 +1,35 @@ +addAsDropdown( + 'menu-entry', + array( + 'icon' => 'menu', + 'label' => t('Add To Menu'), + 'url' => Url::fromPath('navigation/add'), + 'urlParams' => array( + 'url' => rawurlencode(Url::fromRequest()->getRelativeUrl()) + ) + ) + ); + } +} diff --git a/modules/monitoring/application/controllers/AlertsummaryController.php b/modules/monitoring/application/controllers/AlertsummaryController.php index dc28d9035..969131074 100644 --- a/modules/monitoring/application/controllers/AlertsummaryController.php +++ b/modules/monitoring/application/controllers/AlertsummaryController.php @@ -16,6 +16,7 @@ use Icinga\Module\Monitoring\Controller; use Icinga\Module\Monitoring\Web\Widget\SelectBox; use Icinga\Web\Url; use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; class AlertsummaryController extends Controller { @@ -53,7 +54,7 @@ class AlertsummaryController extends Controller 'label' => $this->translate('Alert Summary'), 'url' => Url::fromRequest() ) - )->extend(new DashboardAction())->activate('alertsummary'); + )->extend(new DashboardAction())->extend(new MenuAction())->activate('alertsummary'); $this->view->title = $this->translate('Alert Summary'); $this->view->intervalBox = $this->createIntervalBox(); diff --git a/modules/monitoring/application/controllers/CommentController.php b/modules/monitoring/application/controllers/CommentController.php index df170f1fa..5f61fb79d 100644 --- a/modules/monitoring/application/controllers/CommentController.php +++ b/modules/monitoring/application/controllers/CommentController.php @@ -7,6 +7,7 @@ use Icinga\Module\Monitoring\Controller; use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm; use Icinga\Web\Url; use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; /** * Display detailed information about a comment @@ -55,7 +56,7 @@ class CommentController extends Controller 'title' => $this->translate('Display detailed information about a comment.'), 'url' =>'monitoring/comments/show' ) - )->activate('comment')->extend(new DashboardAction()); + )->activate('comment')->extend(new DashboardAction())->extend(new MenuAction()); } /** diff --git a/modules/monitoring/application/controllers/DowntimeController.php b/modules/monitoring/application/controllers/DowntimeController.php index 946b69985..2b8c9599b 100644 --- a/modules/monitoring/application/controllers/DowntimeController.php +++ b/modules/monitoring/application/controllers/DowntimeController.php @@ -9,6 +9,7 @@ use Icinga\Module\Monitoring\Object\Host; use Icinga\Module\Monitoring\Object\Service; use Icinga\Web\Url; use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; /** * Display detailed information about a downtime @@ -65,7 +66,7 @@ class DowntimeController extends Controller 'title' => $this->translate('Display detailed information about a downtime.'), 'url' =>'monitoring/downtimes/show' ) - )->activate('downtime')->extend(new DashboardAction()); + )->activate('downtime')->extend(new DashboardAction())->extend(new MenuAction()); } /** diff --git a/modules/monitoring/application/controllers/HealthController.php b/modules/monitoring/application/controllers/HealthController.php index bbfdcb030..5905decd5 100644 --- a/modules/monitoring/application/controllers/HealthController.php +++ b/modules/monitoring/application/controllers/HealthController.php @@ -7,6 +7,7 @@ use Icinga\Module\Monitoring\Controller; use Icinga\Module\Monitoring\Forms\Command\Instance\DisableNotificationsExpireCommandForm; use Icinga\Module\Monitoring\Forms\Command\Instance\ToggleInstanceFeaturesCommandForm; use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; /** * Display process and performance information of the monitoring host and program-wide commands @@ -43,7 +44,7 @@ class HealthController extends Controller 'url' =>'monitoring/health/stats' ) ) - ->extend(new DashboardAction()); + ->extend(new DashboardAction())->extend(new MenuAction()); } /** diff --git a/modules/monitoring/application/controllers/HostController.php b/modules/monitoring/application/controllers/HostController.php index 4a3f77a6a..6f0fe720a 100644 --- a/modules/monitoring/application/controllers/HostController.php +++ b/modules/monitoring/application/controllers/HostController.php @@ -77,6 +77,7 @@ class HostController extends MonitoredObjectController 'host_state_type', 'host_last_state_change', 'host_address', + 'host_address6', 'host_handled', 'service_description', 'service_display_name', diff --git a/modules/monitoring/application/controllers/HostsController.php b/modules/monitoring/application/controllers/HostsController.php index 96b78da62..bffbecc05 100644 --- a/modules/monitoring/application/controllers/HostsController.php +++ b/modules/monitoring/application/controllers/HostsController.php @@ -18,6 +18,7 @@ use Icinga\Module\Monitoring\Forms\Command\Object\SendCustomNotificationCommandF use Icinga\Module\Monitoring\Object\HostList; use Icinga\Web\Url; use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; class HostsController extends Controller { @@ -44,7 +45,7 @@ class HostsController extends Controller 'url' => Url::fromRequest(), 'icon' => 'host' ) - )->extend(new DashboardAction())->activate('show'); + )->extend(new DashboardAction())->extend(new MenuAction())->activate('show'); $this->view->listAllLink = Url::fromRequest()->setPath('monitoring/list/hosts'); } @@ -55,6 +56,7 @@ class HostsController extends Controller 'host_icon_image_alt', 'host_name', 'host_address', + 'host_address6', 'host_state', 'host_problem', 'host_handled', @@ -92,6 +94,7 @@ class HostsController extends Controller 'host_icon_image_alt', 'host_name', 'host_address', + 'host_address6', 'host_state', 'host_problem', 'host_handled', diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php index adbe78678..d08257557 100644 --- a/modules/monitoring/application/controllers/ListController.php +++ b/modules/monitoring/application/controllers/ListController.php @@ -13,6 +13,7 @@ use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm; use Icinga\Module\Monitoring\Forms\StatehistoryForm; use Icinga\Web\Url; use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; use Icinga\Web\Widget\Tabextension\OutputFormat; use Icinga\Web\Widget\Tabs; @@ -58,7 +59,6 @@ class ListController extends Controller 'host_name', 'host_display_name', 'host_state' => $stateColumn, - 'host_address', 'host_acknowledged', 'host_output', 'host_attempt', @@ -66,15 +66,10 @@ class ListController extends Controller 'host_is_flapping', 'host_state_type', 'host_handled', - 'host_last_check', 'host_last_state_change' => $stateChangeColumn, 'host_notifications_enabled', - 'host_action_url', - 'host_notes_url', 'host_active_checks_enabled', - 'host_passive_checks_enabled', - 'host_current_check_attempt', - 'host_max_check_attempts' + 'host_passive_checks_enabled' ), $this->addColumns())); $this->applyRestriction('monitoring/filter/objects', $query); $this->filterQuery($query); @@ -132,10 +127,6 @@ class ListController extends Controller 'host_name', 'host_display_name', 'host_state', - 'host_state_type', - 'host_last_state_change', - 'host_address', - 'host_handled', 'service_description', 'service_display_name', 'service_state' => $stateColumn, @@ -152,14 +143,9 @@ class ListController extends Controller 'service_state_type', 'service_handled', 'service_severity', - 'service_last_check', 'service_notifications_enabled', - 'service_action_url', - 'service_notes_url', 'service_active_checks_enabled', - 'service_passive_checks_enabled', - 'current_check_attempt' => 'service_current_check_attempt', - 'max_check_attempts' => 'service_max_check_attempts' + 'service_passive_checks_enabled' ), $this->addColumns()); $query = $this->backend->select()->from('servicestatus', $columns); $this->applyRestriction('monitoring/filter/objects', $query); @@ -628,6 +614,6 @@ class ListController extends Controller */ private function createTabs() { - $this->getTabs()->extend(new OutputFormat())->extend(new DashboardAction()); + $this->getTabs()->extend(new OutputFormat())->extend(new DashboardAction())->extend(new MenuAction()); } } diff --git a/modules/monitoring/application/controllers/ServicesController.php b/modules/monitoring/application/controllers/ServicesController.php index 26561f8ff..bb51a2108 100644 --- a/modules/monitoring/application/controllers/ServicesController.php +++ b/modules/monitoring/application/controllers/ServicesController.php @@ -17,6 +17,7 @@ use Icinga\Module\Monitoring\Forms\Command\Object\SendCustomNotificationCommandF use Icinga\Module\Monitoring\Object\ServiceList; use Icinga\Web\Url; use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; class ServicesController extends Controller { @@ -46,7 +47,7 @@ class ServicesController extends Controller 'url' => Url::fromRequest(), 'icon' => 'services' ) - )->extend(new DashboardAction())->activate('show'); + )->extend(new DashboardAction())->extend(new MenuAction())->activate('show'); } protected function handleCommandForm(ObjectsCommandForm $form) @@ -56,6 +57,7 @@ class ServicesController extends Controller 'host_icon_image_alt', 'host_name', 'host_address', + 'host_address6', 'host_output', 'host_state', 'host_problem', @@ -101,6 +103,7 @@ class ServicesController extends Controller 'host_icon_image_alt', 'host_name', 'host_address', + 'host_address6', 'host_output', 'host_state', 'host_problem', diff --git a/modules/monitoring/application/controllers/TacticalController.php b/modules/monitoring/application/controllers/TacticalController.php index 40641128b..03f2ff37b 100644 --- a/modules/monitoring/application/controllers/TacticalController.php +++ b/modules/monitoring/application/controllers/TacticalController.php @@ -6,6 +6,7 @@ namespace Icinga\Module\Monitoring\Controllers; use Icinga\Module\Monitoring\Controller; use Icinga\Web\Url; use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; class TacticalController extends Controller { @@ -22,7 +23,7 @@ class TacticalController extends Controller 'label' => $this->translate('Tactical Overview'), 'url' => Url::fromRequest() ) - )->extend(new DashboardAction())->activate('tactical_overview'); + )->extend(new DashboardAction())->extend(new MenuAction())->activate('tactical_overview'); $stats = $this->backend->select()->from( 'statussummary', array( diff --git a/modules/monitoring/application/controllers/TimelineController.php b/modules/monitoring/application/controllers/TimelineController.php index 440513e63..39d640f6f 100644 --- a/modules/monitoring/application/controllers/TimelineController.php +++ b/modules/monitoring/application/controllers/TimelineController.php @@ -12,6 +12,7 @@ use Icinga\Module\Monitoring\Web\Widget\SelectBox; use Icinga\Util\Format; use Icinga\Web\Url; use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; class TimelineController extends Controller { @@ -24,7 +25,7 @@ class TimelineController extends Controller 'label' => $this->translate('Timeline'), 'url' => Url::fromRequest() ) - )->extend(new DashboardAction())->activate('timeline'); + )->extend(new DashboardAction())->extend(new MenuAction())->activate('timeline'); $this->view->title = $this->translate('Timeline'); // TODO: filter for hard_states (precedence adjustments necessary!) diff --git a/modules/monitoring/application/views/scripts/show/components/actions.phtml b/modules/monitoring/application/views/scripts/show/components/actions.phtml index 2e55db68c..bffd949dd 100644 --- a/modules/monitoring/application/views/scripts/show/components/actions.phtml +++ b/modules/monitoring/application/views/scripts/show/components/actions.phtml @@ -16,7 +16,11 @@ foreach ($object->getActionUrls() as $i => $link) { 'Action ' . ($i + 1) . $newTabInfo, array( 'url' => $link, - 'target' => '_blank' + 'target' => '_blank', + 'renderer' => array( + 'NavigationItemRenderer', + 'escape_label' => false + ) ) ); } diff --git a/modules/monitoring/application/views/scripts/show/components/customvars.phtml b/modules/monitoring/application/views/scripts/show/components/customvars.phtml index 6ef2c678b..77a611952 100644 --- a/modules/monitoring/application/views/scripts/show/components/customvars.phtml +++ b/modules/monitoring/application/views/scripts/show/components/customvars.phtml @@ -1,6 +1,6 @@ customvars as $name => $value): ?> - escape($name) ?> + escape(ucwords(str_replace('_', ' ', strtolower($name)))) ?> customvar($value) ?> diff --git a/modules/monitoring/application/views/scripts/show/components/notes.phtml b/modules/monitoring/application/views/scripts/show/components/notes.phtml index 1b3dafc52..4aa8b6bfc 100644 --- a/modules/monitoring/application/views/scripts/show/components/notes.phtml +++ b/modules/monitoring/application/views/scripts/show/components/notes.phtml @@ -3,7 +3,7 @@ use Icinga\Web\Navigation\Navigation; $navigation = new Navigation(); -$navigation->load($object->getType() . '-note'); +//$navigation->load($object->getType() . '-note'); foreach ($navigation as $item) { $item->setObject($object); } @@ -26,7 +26,11 @@ if (! empty($links)) { $this->escape($link) . $newTabInfo, array( 'url' => $link, - 'target' => '_blank' + 'target' => '_blank', + 'renderer' => array( + 'NavigationItemRenderer', + 'escape_label' => false + ) ) ); } diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php index 02a2fb8fb..d888daa78 100644 --- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php @@ -28,6 +28,7 @@ class HoststatusQuery extends IdoQuery 'host' => 'ho.name1 COLLATE latin1_general_ci', 'host_action_url' => 'h.action_url', 'host_address' => 'h.address', + 'host_address6' => 'h.address6', 'host_alias' => 'h.alias', 'host_display_name' => 'h.display_name COLLATE latin1_general_ci', 'host_icon_image' => 'h.icon_image', diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php index 122e13dc0..1480e2834 100644 --- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php @@ -832,7 +832,7 @@ abstract class IdoQuery extends DbQuery list($type, $name) = $this->customvarNameToTypeName($customvar); $alias = ($type === 'host' ? 'hcv_' : 'scv_') . $name; - $this->customVars[$customvar] = $alias; + $this->customVars[strtolower($customvar)] = $alias; if ($this->hasJoinedVirtualTable('services')) { $leftcol = 's.' . $type . '_object_id'; diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php index a4b1d7272..f2582d211 100644 --- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php @@ -27,6 +27,7 @@ class ServicestatusQuery extends IdoQuery 'hosts' => array( 'host_action_url' => 'h.action_url', 'host_address' => 'h.address', + 'host_address6' => 'h.address6', 'host_alias' => 'h.alias COLLATE latin1_general_ci', 'host_display_name' => 'h.display_name COLLATE latin1_general_ci', 'host_icon_image' => 'h.icon_image', diff --git a/modules/monitoring/library/Monitoring/Controller.php b/modules/monitoring/library/Monitoring/Controller.php index 1dc1d28eb..e1c16c854 100644 --- a/modules/monitoring/library/Monitoring/Controller.php +++ b/modules/monitoring/library/Monitoring/Controller.php @@ -82,7 +82,7 @@ class Controller extends IcingaWebController 'service_description', 'servicegroup_name', function ($c) { - return preg_match('/^_(?:host|service)_/', $c); + return preg_match('/^_(?:host|service)_/i', $c); } )); foreach ($this->getRestrictions($name) as $filter) { diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php index 845aa4db4..50cd436fd 100644 --- a/modules/monitoring/library/Monitoring/DataView/DataView.php +++ b/modules/monitoring/library/Monitoring/DataView/DataView.php @@ -185,7 +185,11 @@ abstract class DataView implements QueryInterface, SortRules, FilterColumns, Ite */ public function isValidFilterTarget($column) { - return in_array($column, $this->getFilterColumns()); + // Customvar + if ($column[0] === '_' && preg_match('/^_(?:host|service)_/i', $column)) { + return true; + } + return in_array($column, $this->getColumns()) || in_array($column, $this->getStaticFilterColumns()); } /** diff --git a/modules/monitoring/library/Monitoring/DataView/Hoststatus.php b/modules/monitoring/library/Monitoring/DataView/Hoststatus.php index f30e00193..f2fe139ec 100644 --- a/modules/monitoring/library/Monitoring/DataView/Hoststatus.php +++ b/modules/monitoring/library/Monitoring/DataView/Hoststatus.php @@ -16,6 +16,7 @@ class HostStatus extends DataView 'host_display_name', 'host_alias', 'host_address', + 'host_address6', 'host_state', 'host_state_type', 'host_handled', diff --git a/modules/monitoring/library/Monitoring/DataView/Servicestatus.php b/modules/monitoring/library/Monitoring/DataView/Servicestatus.php index 8c19cb1ab..ff63e8604 100644 --- a/modules/monitoring/library/Monitoring/DataView/Servicestatus.php +++ b/modules/monitoring/library/Monitoring/DataView/Servicestatus.php @@ -18,6 +18,7 @@ class ServiceStatus extends DataView 'host_state_type', 'host_last_state_change', 'host_address', + 'host_address6', 'host_problem', 'host_handled', 'service_description', diff --git a/modules/monitoring/library/Monitoring/Object/Host.php b/modules/monitoring/library/Monitoring/Object/Host.php index 75c31c3ea..49f2f4306 100644 --- a/modules/monitoring/library/Monitoring/Object/Host.php +++ b/modules/monitoring/library/Monitoring/Object/Host.php @@ -95,6 +95,7 @@ class Host extends MonitoredObject 'host_active_checks_enabled', 'host_active_checks_enabled_changed', 'host_address', + 'host_address6', 'host_alias', 'host_attempt', 'host_check_command', diff --git a/modules/monitoring/library/Monitoring/Object/Macro.php b/modules/monitoring/library/Monitoring/Object/Macro.php index 0a6f3a8c0..73e982b15 100644 --- a/modules/monitoring/library/Monitoring/Object/Macro.php +++ b/modules/monitoring/library/Monitoring/Object/Macro.php @@ -14,11 +14,13 @@ class Macro * @var array */ private static $icingaMacros = array( - 'HOSTNAME' => 'host_name', - 'HOSTADDRESS' => 'host_address', - 'SERVICEDESC' => 'service_description', - 'host.name' => 'host_name', - 'host.address' => 'host_address', + 'HOSTNAME' => 'host_name', + 'HOSTADDRESS' => 'host_address', + 'HOSTADDRESS6' => 'host_address6', + 'SERVICEDESC' => 'service_description', + 'host.name' => 'host_name', + 'host.address' => 'host_address', + 'host.address6' => 'host_address6', 'service.description' => 'service_description' ); @@ -58,8 +60,9 @@ class Macro if (isset(self::$icingaMacros[$macro]) && isset($object->{self::$icingaMacros[$macro]})) { return $object->{self::$icingaMacros[$macro]}; } - if (isset($object->customvars[$macro])) { - return $object->customvars[$macro]; + $customVar = strtolower($macro); + if (isset($object->customvars[$customVar])) { + return $object->customvars[$customVar]; } return $macro; diff --git a/modules/monitoring/library/Monitoring/Object/MonitoredObject.php b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php index 98ada34de..866e57c60 100644 --- a/modules/monitoring/library/Monitoring/Object/MonitoredObject.php +++ b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php @@ -239,7 +239,7 @@ abstract class MonitoredObject implements Filterable foreach ($this->customvars as $name => $value) { if (! is_object($value)) { - $row->{'_' . $this->getType() . '_' . strtolower(str_replace(' ', '_', $name))} = $value; + $row->{'_' . $this->getType() . '_' . $name} = $value; } } } @@ -477,8 +477,8 @@ abstract class MonitoredObject implements Filterable $this->customvars = array(); $customvars = $query->getQuery()->fetchAll(); - foreach ($customvars as $name => $cv) { - $name = ucwords(str_replace('_', ' ', strtolower($cv->varname))); + foreach ($customvars as $cv) { + $name = strtolower($cv->varname); if ($blacklistPattern && preg_match($blacklistPattern, $cv->varname)) { $this->customvars[$name] = '***'; } elseif ($cv->is_json) { diff --git a/modules/monitoring/library/Monitoring/Object/Service.php b/modules/monitoring/library/Monitoring/Object/Service.php index f4f3ce748..ee75706ea 100644 --- a/modules/monitoring/library/Monitoring/Object/Service.php +++ b/modules/monitoring/library/Monitoring/Object/Service.php @@ -112,6 +112,7 @@ class Service extends MonitoredObject 'host_acknowledged', 'host_active_checks_enabled', 'host_address', + 'host_address6', 'host_alias', 'host_display_name', 'host_handled', diff --git a/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php b/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php index 5ccadf5a6..9fdaff259 100644 --- a/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php +++ b/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php @@ -13,6 +13,7 @@ use Icinga\Module\Monitoring\Forms\Command\Object\ToggleObjectFeaturesCommandFor use Icinga\Web\Hook; use Icinga\Web\Url; use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; /** * Base class for the host and service controller @@ -232,6 +233,6 @@ abstract class MonitoredObjectController extends Controller ) ); } - $tabs->extend(new DashboardAction()); + $tabs->extend(new DashboardAction())->extend(new MenuAction()); } } diff --git a/modules/monitoring/test/php/library/Monitoring/Object/MacroTest.php b/modules/monitoring/test/php/library/Monitoring/Object/MacroTest.php index e1b3595e3..89dae3b4d 100644 --- a/modules/monitoring/test/php/library/Monitoring/Object/MacroTest.php +++ b/modules/monitoring/test/php/library/Monitoring/Object/MacroTest.php @@ -16,11 +16,14 @@ class MacroTest extends BaseTestCase $hostMock = Mockery::mock('host'); $hostMock->host_name = 'test'; $hostMock->host_address = '1.1.1.1'; + $hostMock->host_address6 = '::1'; $this->assertEquals(Macro::resolveMacros('$HOSTNAME$', $hostMock), $hostMock->host_name); $this->assertEquals(Macro::resolveMacros('$HOSTADDRESS$', $hostMock), $hostMock->host_address); + $this->assertEquals(Macro::resolveMacros('$HOSTADDRESS6$', $hostMock), $hostMock->host_address6); $this->assertEquals(Macro::resolveMacros('$host.name$', $hostMock), $hostMock->host_name); $this->assertEquals(Macro::resolveMacros('$host.address$', $hostMock), $hostMock->host_address); + $this->assertEquals(Macro::resolveMacros('$host.address6$', $hostMock), $hostMock->host_address6); } public function testServiceMacros() @@ -28,13 +31,16 @@ class MacroTest extends BaseTestCase $svcMock = Mockery::mock('service'); $svcMock->host_name = 'test'; $svcMock->host_address = '1.1.1.1'; + $svcMock->host_address6 = '::1'; $svcMock->service_description = 'a service'; $this->assertEquals(Macro::resolveMacros('$HOSTNAME$', $svcMock), $svcMock->host_name); $this->assertEquals(Macro::resolveMacros('$HOSTADDRESS$', $svcMock), $svcMock->host_address); + $this->assertEquals(Macro::resolveMacros('$HOSTADDRESS6$', $svcMock), $svcMock->host_address6); $this->assertEquals(Macro::resolveMacros('$SERVICEDESC$', $svcMock), $svcMock->service_description); $this->assertEquals(Macro::resolveMacros('$host.name$', $svcMock), $svcMock->host_name); $this->assertEquals(Macro::resolveMacros('$host.address$', $svcMock), $svcMock->host_address); + $this->assertEquals(Macro::resolveMacros('$host.address6$', $svcMock), $svcMock->host_address6); $this->assertEquals(Macro::resolveMacros('$service.description$', $svcMock), $svcMock->service_description); } diff --git a/modules/setup/application/views/scripts/index/parts/finish.phtml b/modules/setup/application/views/scripts/index/parts/finish.phtml index a56c07c3a..847ae5b92 100644 --- a/modules/setup/application/views/scripts/index/parts/finish.phtml +++ b/modules/setup/application/views/scripts/index/parts/finish.phtml @@ -8,7 +8,7 @@ qlink( $this->translate('Login to Icinga Web 2'), - 'authentication/login', + 'authentication/login?renderLayout', null, array( 'class' => 'button-like login', @@ -30,4 +30,4 @@
-
\ No newline at end of file +
diff --git a/public/css/icinga/about.less b/public/css/icinga/about.less new file mode 100644 index 000000000..320f2f4de --- /dev/null +++ b/public/css/icinga/about.less @@ -0,0 +1,9 @@ +div.about { + width: 600px; + margin: 0 auto; + text-align: center; + + .about-modules { + text-align: initial; + } +} \ No newline at end of file diff --git a/public/css/icinga/layout-structure.less b/public/css/icinga/layout-structure.less index 3d2cb8774..7a850f8b6 100644 --- a/public/css/icinga/layout-structure.less +++ b/public/css/icinga/layout-structure.less @@ -271,14 +271,7 @@ html { } #login { - .logo .image img { - width: 70%; - } - .form { - width: 100%; - margin: auto; - } - .form label { + .below-logo label { width: 100%; margin: 0; text-align: center; diff --git a/public/css/icinga/logo.less b/public/css/icinga/logo.less new file mode 100644 index 000000000..68fb81619 --- /dev/null +++ b/public/css/icinga/logo.less @@ -0,0 +1,47 @@ +/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ + +.logo { + background-color: @colorPetrol; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 60%; + border-bottom: 1px solid #d9d9d9d; + text-align: center; + -webkit-box-shadow: 0 3px 7px -3px #000; + -moz-box-shadow: 0 3px 7px -3px #000; + box-shadow: 0 3px 7px -3px #000; + + .image { + position: absolute; + bottom: 1em; + left: 0px; + right: 0px; + text-align: center; + + img { + width: 375px; + } + } +} + +.below-logo { + position: absolute; + font-size: 0.9em; + top: 45%; + left: 0; + bottom: 0; + right: 0; +} + +#layout.minimal-layout { + .logo .image img { + width: 70%; + } + + .below-logo { + width: 100%; + margin: auto; + } +} diff --git a/public/img/bugreport.png b/public/img/bugreport.png new file mode 100644 index 000000000..23d913291 Binary files /dev/null and b/public/img/bugreport.png differ diff --git a/public/img/docs.png b/public/img/docs.png new file mode 100644 index 000000000..051db6cce Binary files /dev/null and b/public/img/docs.png differ diff --git a/public/img/support.png b/public/img/support.png new file mode 100644 index 000000000..17a0c9233 Binary files /dev/null and b/public/img/support.png differ diff --git a/public/img/wiki.png b/public/img/wiki.png new file mode 100644 index 000000000..02df73af6 Binary files /dev/null and b/public/img/wiki.png differ diff --git a/public/js/icinga/behavior/selectable.js b/public/js/icinga/behavior/selectable.js new file mode 100644 index 000000000..a628ed729 --- /dev/null +++ b/public/js/icinga/behavior/selectable.js @@ -0,0 +1,36 @@ +/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ + +;(function(Icinga, $) { + 'use strict'; + + Icinga.Behaviors = Icinga.Behaviors || {}; + + var Selectable = function(icinga) { + Icinga.EventListener.call(this, icinga); + this.on('rendered', this.onRendered, this); + }; + + $.extend(Selectable.prototype, new Icinga.EventListener(), { + onRendered: function(e) { + $('.selectable', e.target).on('dblclick', e.data.self.selectText); + }, + + selectText: function(e) { + var b = document.body, + r; + if (b.createTextRange) { + r = b.createTextRange(); + r.moveToElementText(e.target); + r.select(); + } else if (window.getSelection) { + var s = window.getSelection(); + r = document.createRange(); + r.selectNodeContents(e.target); + s.removeAllRanges(); + s.addRange(r); + } + } + }); + + Icinga.Behaviors.Selectable = Selectable; +})(Icinga, jQuery);