Merge branch 'master' into feature/dope-layout-5543

This commit is contained in:
Eric Lippmann 2015-09-30 18:52:52 +02:00
commit 231c63b703
72 changed files with 1147 additions and 382 deletions

View File

@ -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'],
}

View File

@ -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'],
}
}

View File

@ -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:

View File

@ -1 +1 @@
$Format:%H%d %ci$
$Format:%H %ci$

View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -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);
}
/**

View File

@ -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) {

View File

@ -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',

View File

@ -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);
}
/**

View File

@ -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;
}
}

View File

@ -0,0 +1,8 @@
<div class="logo">
<div class="image">
<img aria-hidden="true" src="<?= $this->baseUrl('img/logo_icinga_big.png'); ?>" alt="<?= $this->translate('The Icinga logo'); ?>" >
</div>
</div>
<div class="below-logo">
<?= $this->render('inline.phtml') ?>
</div>

View File

@ -1,25 +1,128 @@
<div class="content">
<h1>Icinga Web 2</h1>
<?php
$versionInfo = array();
if ($version !== false) {
foreach (array(
'appVersion' => $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)
? '<p class="message-error">' . $this->translate(
'Can\'t determine Icinga Web 2\'s version'
)
: '<p>' . nl2br(implode("\n", $versionInfo), false)
) . '</p>';
?>
<div class="controls">
<?= $tabs; ?>
</div>
<div class="content about">
<?= $this->img(
'img/logo_icinga_big_dark.png',
null,
array(
'width' => 400,
'class' => 'about-logo'
)
); ?>
<p class="about-version">
<?php if (isset($version['appVersion'])): ?>
<strong><?= $this->translate('Version'); ?>:</strong> <?= $this->escape($version['appVersion']); ?>
<?php endif ?>
<?php if (isset($version['gitCommitID'])): ?>
<br>
<strong><?= $this->translate('Git commit ID'); ?>:</strong> <?= $this->escape($version['gitCommitID']); ?>
<?php endif ?>
<?php if (isset($version['gitCommitDate'])): ?>
<br>
<strong><?= $this->translate('Git commit date'); ?>:</strong> <?= $this->escape($version['gitCommitDate']); ?>
<?php endif ?>
</p>
<p class="about-license">
<strong><?= $this->translate('Copyright'); ?></strong>: &copy; 2013-<?= date('Y'); ?> <?= $this->qlink(
$this->translate('The Icinga Project'),
'https://www.icinga.org',
null,
array(
'target' => '_blank'
)
); ?>
<br>
<strong><?= $this->translate('License'); ?></strong>: GNU GPL v2+
</p>
<p class="about-social">
<?= $this->qlink(
null,
'https://www.twitter.com/icinga',
null,
array(
'target' => '_blank',
'icon' => 'twitter',
'title' => $this->translate('Icinga on Twitter')
)
); ?> <?= $this->qlink(
null,
'https://www.facebook.com/icinga',
null,
array(
'target' => '_blank',
'icon' => 'facebook-squared',
'title' => $this->translate('Icinga on Facebook')
)
); ?>
</p>
<div class="about-urls">
<div><?= $this->qlink(
null,
'https://dev.icinga.org/projects/icingaweb2',
null,
array(
'target' => '_blank',
'img' => 'img/bugreport.png',
'title' => $this->translate('Report a bug')
)
); ?> <?= $this->qlink(
null,
'https://www.icinga.org/services/support',
null,
array(
'target' => '_blank',
'img' => 'img/support.png',
'title' => $this->translate('Support / Mailinglists')
)
); ?></div>
<div><?= $this->qlink(
null,
'https://wiki.icinga.org',
null,
array(
'target' => '_blank',
'img' => 'img/wiki.png',
'title' => $this->translate('Icinga Wiki')
)
); ?> <?= $this->qlink(
null,
'https://docs.icinga.org/',
null,
array(
'target' => '_blank',
'img' => 'img/docs.png',
'title' => $this->translate('Icinga Documentation')
)
); ?></div>
</div>
<h2><?= $this->translate('Loaded modules') ?></h2>
<table class="action alternating about-modules" data-base-target="_next">
<thead>
<tr>
<th><?= $this->translate('Name') ?></th>
<th><?= $this->translate('Version') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($modules as $module): ?>
<tr>
<td>
<?php if ($this->hasPermission('config/modules')): ?>
<?= $this->qlink(
$module->getName(),
'config/module/',
array('name' => $module->getName()),
array('title' => sprintf($this->translate('Show the overview of the %s module'), $module->getName()))
); ?>
<?php else: ?>
<?= $this->escape($module->getName()); ?>
<?php endif ?>
<td>
<?= $this->escape($module->getVersion()); ?>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>

View File

@ -1,10 +1,12 @@
<?php if (! $hideControls): ?>
<div class="controls">
<?= $this->tabs->showOnlyCloseButton() ?>
<?= $tabs->showOnlyCloseButton(); ?>
</div>
<div class="content">
<p><strong><?= nl2br($this->escape($message)) ?></strong></p>
<?php if (isset($stackTrace)) : ?>
<hr />
<pre><?= $this->escape($stackTrace) ?></pre>
<?php endif ?>
</div>
<div class="content">
<p><strong><?= nl2br($this->escape($message)); ?></strong></p>
<?php if (isset($stackTrace)): ?>
<hr />
<pre><?= $this->escape($stackTrace) ?></pre>
<?php endif ?>
</div>

View File

@ -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): ?>
<tbody>
<?php endif ?>
<tr>
<td class="member-name"><?= $this->escape($member->user_name); ?></td>
<td class="member-name">
<?php if (
$this->hasPermission('config/authentication/users/show')
&& ($userBackend = $backend->getUserBackend()) !== null
&& $userBackend instanceof Selectable
): ?>
<?= $this->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)
)); ?>
<?php else: ?>
<?= $this->escape($member->user_name); ?>
<?php endif ?>
</td>
<?php if (isset($removeForm)): ?>
<td class="member-remove" data-base-target="_self">
<?php $removeForm->getElement('user_name')->setValue($member->user_name); echo $removeForm; ?>

View File

@ -22,14 +22,17 @@
<th style="width: 5em"><?= $this->translate('Remove'); ?></th>
</thead>
<tbody>
<?php foreach ($items as $name => $item): ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?= $this->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)
)
); ?></td>
<td><?= $item->type && isset($types[$item->type])
@ -39,10 +42,13 @@
<td><?= $this->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)
)
); ?></td>
</tr>

View File

@ -1,4 +1,8 @@
<?php if (! $this->compact): ?>
<?php
use Icinga\Web\Url;
if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs; ?>
<?= $this->sortBox; ?>
@ -19,17 +23,19 @@
<th style="width: 5em"><?= $this->translate('Unshare'); ?></th>
</thead>
<tbody>
<?php foreach ($items as $name => $item): ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?= $this->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)
)
); ?></td>
<td><?= $item->type && isset($types[$item->type])
@ -48,7 +54,12 @@
)
); ?></td>
<?php else: ?>
<td data-base-target="_self"><?= $removeForm->setDefault('name', $name); ?></td>
<td data-base-target="_self"><?= $removeForm
->setDefault('name', $item->name)
->setAction(Url::fromPath(
'navigation/unshare',
array('type' => $item->type, 'owner' => $item->owner)
)); ?></td>
<?php endif ?>
</tr>
<?php endforeach ?>

View File

@ -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

View File

@ -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
*

View File

@ -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;
}

View File

@ -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(
'/(?<!.)\s*(?P<gitCommitID>\w+)(?:\s*\(.*?(?:(?<=[\(,])\s*tag\s*:\s*v(?P<appVersion>.+?)\s*(?=[\),]).*?)?\))?\s*(?P<gitCommitDate>\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<gitCommitID>\w+) (?P<gitCommitDate>\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('/(?<!.)ref:\s+(.+?)$/ms', $gitHead, $matches)) {
$gitCommitID = @file_get_contents($gitDir . DIRECTORY_SEPARATOR . $matches[1]);
} else {
$gitCommitID = $gitHead;
}
if (false !== $gitCommitID) {
$matches = array();
if (@preg_match('/(?<!.)(?P<gitCommitID>[0-9a-f]+)$/ms', $gitCommitID, $matches)) {
return array_merge($version, $matches);
}
}
}
return $version;
}
}

View File

@ -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;
}

View File

@ -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'
));
}
}
}

View File

@ -80,7 +80,7 @@ class ArrayDatasource implements Selectable
*/
public function select()
{
return new SimpleQuery($this);
return new SimpleQuery(clone $this);
}
/**

View File

@ -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;
}

View File

@ -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
*

View File

@ -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;
}
}
/**
* 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]);
}
}

View File

@ -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;

View File

@ -1,5 +1,6 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web;
class FileCache
@ -191,7 +192,7 @@ class FileCache
* Whether the given ETag matchesspecific file(s) on disk
*
* If no ETag is given we'll try to fetch the one from the current
* HTTP request.
* HTTP request. Respects HTTP Cache-Control: no-cache, if set.
*
* @param string|array $files file(s) to check
* @param string $match ETag to match against
@ -208,6 +209,9 @@ class FileCache
if (! $match) {
return false;
}
if (isset($_SERVER['HTTP_CACHE_CONTROL']) && $_SERVER['HTTP_CACHE_CONTROL'] === 'no-cache') {
return false;
}
$etag = self::etagForFiles($files);
return $match === $etag ? $etag : false;

View File

@ -27,7 +27,8 @@ class JavaScript
'js/icinga/behavior/tristate.js',
'js/icinga/behavior/navigation.js',
'js/icinga/behavior/form.js',
'js/icinga/behavior/actiontable.js'
'js/icinga/behavior/actiontable.js',
'js/icinga/behavior/selectable.js'
);
protected static $vendorFiles = array(

View File

@ -429,12 +429,14 @@ class Navigation implements ArrayAccess, Countable, IteratorAggregate
*/
public function load($type)
{
// Shareables
$this->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
*

View File

@ -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;
}

View File

@ -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()

View File

@ -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;
}

View File

@ -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(

View File

@ -85,6 +85,7 @@ class Dashboard extends AbstractWidget
}
$this->mergePanes($panes);
$this->loadUserDashboards();
return $this;
}

View File

@ -0,0 +1,35 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Widget\Tabextension;
use Icinga\Web\Url;
use Icinga\Web\Widget\Tabs;
/**
* Tabextension that allows to add the current URL as menu entry
*
* Displayed as a dropdown field in the tabs
*/
class MenuAction implements Tabextension
{
/**
* Applies the menu actions to the provided tabset
*
* @param Tabs $tabs The tabs object to extend with
*/
public function apply(Tabs $tabs)
{
$tabs->addAsDropdown(
'menu-entry',
array(
'icon' => 'menu',
'label' => t('Add To Menu'),
'url' => Url::fromPath('navigation/add'),
'urlParams' => array(
'url' => rawurlencode(Url::fromRequest()->getRelativeUrl())
)
)
);
}
}

View File

@ -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();

View File

@ -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());
}
/**

View File

@ -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());
}
/**

View File

@ -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());
}
/**

View File

@ -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',

View File

@ -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',

View File

@ -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());
}
}

View File

@ -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',

View File

@ -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(

View File

@ -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!)

View File

@ -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
)
)
);
}

View File

@ -1,6 +1,6 @@
<?php foreach ($object->customvars as $name => $value): ?>
<tr>
<th><?= $this->escape($name) ?></th>
<th><?= $this->escape(ucwords(str_replace('_', ' ', strtolower($name)))) ?></th>
<td><?= $this->customvar($value) ?></td>
</tr>
<?php endforeach ?>

View File

@ -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
)
)
);
}

View File

@ -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',

View File

@ -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';

View File

@ -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',

View File

@ -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) {

View File

@ -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());
}
/**

View File

@ -16,6 +16,7 @@ class HostStatus extends DataView
'host_display_name',
'host_alias',
'host_address',
'host_address6',
'host_state',
'host_state_type',
'host_handled',

View File

@ -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',

View File

@ -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',

View File

@ -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;

View File

@ -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) {

View File

@ -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',

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -8,7 +8,7 @@
<?php if ($success): ?>
<?= $this->qlink(
$this->translate('Login to Icinga Web 2'),
'authentication/login',
'authentication/login?renderLayout',
null,
array(
'class' => 'button-like login',
@ -30,4 +30,4 @@
<pre class="log-output"><?= join("\n\n", array_map(function($a) {
return join("\n", $a);
}, $report)); ?></pre>
</div>
</div>

View File

@ -0,0 +1,9 @@
div.about {
width: 600px;
margin: 0 auto;
text-align: center;
.about-modules {
text-align: initial;
}
}

View File

@ -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;

View File

@ -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;
}
}

BIN
public/img/bugreport.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
public/img/docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/img/support.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
public/img/wiki.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -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);