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

This commit is contained in:
Eric Lippmann 2015-09-27 13:11:11 +02:00
commit d3ac1b16ee
86 changed files with 4921 additions and 754 deletions

View File

@ -3,11 +3,8 @@
namespace Icinga\Controllers; namespace Icinga\Controllers;
use Icinga\Application\Icinga;
use Icinga\Web\Controller\ActionController; use Icinga\Web\Controller\ActionController;
use Icinga\Web\Hook;
use Icinga\Web\Menu;
use Icinga\Web\MenuRenderer;
use Icinga\Web\Url;
/** /**
* Create complex layout parts * Create complex layout parts
@ -21,9 +18,6 @@ class LayoutController extends ActionController
{ {
$this->setAutorefreshInterval(15); $this->setAutorefreshInterval(15);
$this->_helper->layout()->disableLayout(); $this->_helper->layout()->disableLayout();
$this->view->menuRenderer = Icinga::app()->getMenu()->getRenderer();
$url = Url::fromRequest();
$menu = new MenuRenderer(Menu::load(), $url->getRelativeUrl());
$this->view->menuRenderer = $menu->useCustomRenderer();
} }
} }

View File

@ -0,0 +1,346 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
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\Notification;
use Icinga\Web\Url;
/**
* Navigation configuration
*/
class NavigationController extends Controller
{
/**
* The default item types provided by Icinga Web 2
*
* @var array
*/
protected $defaultItemTypes;
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
$this->defaultItemTypes = array(
'menu-item' => $this->translate('Menu Entry'),
'dashlet' => 'Dashlet'
);
}
/**
* Return a list of available navigation item types
*
* @return array
*/
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);
}
}
}
return $types;
}
/**
* 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())
));
$ds->setKeyColumn('name');
$query = $ds->select();
$this->view->types = $this->listItemTypes();
$this->view->items = $query;
$this->getTabs()->add(
'navigation',
array(
'title' => $this->translate('List and configure your own navigation items'),
'label' => $this->translate('Navigation'),
'url' => 'navigation'
)
)->activate('navigation');
$this->setupSortControl(
array(
'type' => $this->translate('Type'),
'owner' => $this->translate('Shared'),
'name' => $this->translate('Shared Navigation')
),
$query
);
}
/**
* List all shared navigation items
*/
public function sharedAction()
{
$this->assertPermission('config/application/navigation');
$config = Config::app('navigation');
$config->getConfigObject()->setKeyColumn('name');
$query = $config->select();
$removeForm = new Form();
$removeForm->setUidDisabled();
$removeForm->setAction(Url::fromPath('navigation/unshare'));
$removeForm->addElement('hidden', 'name', array(
'decorators' => array('ViewHelper')
));
$removeForm->addElement('hidden', 'redirect', array(
'value' => Url::fromPath('navigation/shared'),
'decorators' => array('ViewHelper')
));
$removeForm->addElement('button', 'btn_submit', array(
'escape' => false,
'type' => 'submit',
'class' => 'link-like spinner',
'value' => 'btn_submit',
'decorators' => array('ViewHelper'),
'label' => $this->view->icon('trash'),
'title' => $this->translate('Unshare this navigation item')
));
$this->view->removeForm = $removeForm;
$this->view->types = $this->listItemTypes();
$this->view->items = $query;
$this->getTabs()->add(
'navigation/shared',
array(
'title' => $this->translate('List and configure shared navigation items'),
'label' => $this->translate('Shared Navigation'),
'url' => 'navigation/shared'
)
)->activate('navigation/shared');
$this->setupSortControl(
array(
'type' => $this->translate('Type'),
'owner' => $this->translate('Owner'),
'name' => $this->translate('Shared Navigation')
),
$query
);
}
/**
* Add a navigation item
*/
public function addAction()
{
$form = new NavigationConfigForm();
$form->setRedirectUrl('navigation');
$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'));
$form->setOnSuccess(function (NavigationConfigForm $form) {
$data = array_filter($form->getValues());
try {
$form->add($data);
} catch (Exception $e) {
$form->error($e->getMessage());
return false;
}
if ($form->save()) {
if (isset($data['type']) && $data['type'] === 'menu-item') {
$form->getResponse()->setRerenderLayout();
}
Notification::success(t('Navigation item successfully created'));
return true;
}
return false;
});
$form->handleRequest();
$this->view->form = $form;
$this->render('form');
}
/**
* Edit a navigation item
*/
public function editAction()
{
$itemName = $this->params->getRequired('name');
$referrer = $this->params->get('referrer', 'index');
$form = new NavigationConfigForm();
$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->setOnSuccess(function (NavigationConfigForm $form) use ($itemName) {
$data = array_map(
function ($v) {
return $v !== '' ? $v : null;
},
$form->getValues()
);
try {
$form->edit($itemName, $data);
} catch (NotFoundError $e) {
throw $e;
} catch (Exception $e) {
$form->error($e->getMessage());
return false;
}
if ($form->save()) {
if (isset($data['type']) && $data['type'] === 'menu-item') {
$form->getResponse()->setRerenderLayout();
}
Notification::success(sprintf(t('Navigation item "%s" successfully updated'), $itemName));
return true;
}
return false;
});
try {
$form->load($itemName);
$form->handleRequest();
} catch (NotFoundError $_) {
$this->httpNotFound(sprintf($this->translate('Navigation item "%s" not found'), $itemName));
}
$this->view->form = $form;
$this->render('form');
}
/**
* Remove a navigation item
*/
public function removeAction()
{
$itemName = $this->params->getRequired('name');
$navigationConfigForm = new NavigationConfigForm();
$navigationConfigForm->setUser($this->Auth()->getUser());
$navigationConfigForm->setShareConfig(Config::app('navigation'));
$form = new ConfirmRemovalForm();
$form->setRedirectUrl('navigation');
$form->setTitle(sprintf($this->translate('Remove Navigation Item %s'), $itemName));
$form->setOnSuccess(function (ConfirmRemovalForm $form) use ($itemName, $navigationConfigForm) {
try {
$itemConfig = $navigationConfigForm->delete($itemName);
} catch (NotFoundError $e) {
Notification::success(sprintf(t('Navigation Item "%s" not found. No action required'), $itemName));
return true;
} catch (Exception $e) {
$form->error($e->getMessage());
return false;
}
if ($navigationConfigForm->save()) {
if ($itemConfig->type === 'menu-item') {
$form->getResponse()->setRerenderLayout();
}
Notification::success(sprintf(t('Navigation Item "%s" successfully removed'), $itemName));
return true;
}
return false;
});
$form->handleRequest();
$this->view->form = $form;
$this->render('form');
}
/**
* Unshare a navigation item
*/
public function unshareAction()
{
$this->assertPermission('config/application/navigation');
$this->assertHttpMethod('POST');
$navigationConfigForm = new NavigationConfigForm();
$navigationConfigForm->setUser($this->Auth()->getUser());
$navigationConfigForm->setShareConfig(Config::app('navigation'));
$form = new Form(array(
'onSuccess' => function ($form) use ($navigationConfigForm) {
$itemName = $form->getValue('name');
try {
$newConfig = $navigationConfigForm->unshare($itemName);
if ($navigationConfigForm->save()) {
if ($newConfig->getSection($itemName)->type === 'menu-item') {
$form->getResponse()->setRerenderLayout();
}
Notification::success(sprintf(
t('Navigation item "%s" has been unshared'),
$form->getValue('name')
));
} else {
// TODO: It failed obviously to write one of the configs, so we're leaving the user in
// a inconsistent state. Luckily, it's nothing lost but possibly duplicated...
Notification::error(sprintf(
t('Failed to unshare navigation item "%s"'),
$form->getValue('name')
));
}
} catch (NotFoundError $e) {
throw $e;
} catch (Exception $e) {
Notification::error($e->getMessage());
}
$redirect = $form->getValue('redirect');
if (! empty($redirect)) {
$form->setRedirectUrl(htmlspecialchars_decode($redirect));
}
return true;
}
));
$form->setUidDisabled();
$form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called
$form->addElement('hidden', 'name', array('required' => true));
$form->addElement('hidden', 'redirect');
try {
$form->handleRequest();
} catch (NotFoundError $_) {
$this->httpNotFound(sprintf($this->translate('Navigation item "%s" not found'), $form->getValue('name')));
}
}
}

View File

@ -32,7 +32,7 @@ class PreferenceController extends BasePreferenceController
array( array(
'title' => t('Adjust the preferences of Icinga Web 2 according to your needs'), 'title' => t('Adjust the preferences of Icinga Web 2 according to your needs'),
'label' => t('Preferences'), 'label' => t('Preferences'),
'url' => Url::fromPath('/preference') 'url' => Url::fromPath('preference')
) )
) )
); );

View File

@ -53,20 +53,6 @@ class DbBackendForm extends Form
'label' => $this->translate('Backend Name'), 'label' => $this->translate('Backend Name'),
'description' => $this->translate( 'description' => $this->translate(
'The name of this authentication provider that is used to differentiate it from others' 'The name of this authentication provider that is used to differentiate it from others'
),
'validators' => array(
array(
'Regex',
false,
array(
'pattern' => '/^[^\\[\\]:]+$/',
'messages' => array(
'regexNotMatch' => $this->translate(
'The name cannot contain \'[\', \']\' or \':\'.'
)
)
)
)
) )
) )
); );

View File

@ -32,20 +32,6 @@ class ExternalBackendForm extends Form
'label' => $this->translate('Backend Name'), 'label' => $this->translate('Backend Name'),
'description' => $this->translate( 'description' => $this->translate(
'The name of this authentication provider that is used to differentiate it from others' 'The name of this authentication provider that is used to differentiate it from others'
),
'validators' => array(
array(
'Regex',
false,
array(
'pattern' => '/^[^\\[\\]:]+$/',
'messages' => array(
'regexNotMatch' => $this->translate(
'The backend name cannot contain \'[\', \']\' or \':\'.'
)
)
)
)
) )
) )
); );

View File

@ -57,20 +57,6 @@ class LdapBackendForm extends Form
'label' => $this->translate('Backend Name'), 'label' => $this->translate('Backend Name'),
'description' => $this->translate( 'description' => $this->translate(
'The name of this authentication provider that is used to differentiate it from others.' 'The name of this authentication provider that is used to differentiate it from others.'
),
'validators' => array(
array(
'Regex',
false,
array(
'pattern' => '/^[^\\[\\]:]+$/',
'messages' => array(
'regexNotMatch' => $this->translate(
'The name cannot contain \'[\', \']\' or \':\'.'
)
)
)
)
) )
) )
); );

View File

@ -34,20 +34,6 @@ class DbUserGroupBackendForm extends Form
'label' => $this->translate('Backend Name'), 'label' => $this->translate('Backend Name'),
'description' => $this->translate( 'description' => $this->translate(
'The name of this user group backend that is used to differentiate it from others' 'The name of this user group backend that is used to differentiate it from others'
),
'validators' => array(
array(
'Regex',
false,
array(
'pattern' => '/^[^\\[\\]:]+$/',
'messages' => array(
'regexNotMatch' => $this->translate(
'The name cannot contain \'[\', \']\' or \':\'.'
)
)
)
)
) )
) )
); );

View File

@ -39,20 +39,6 @@ class LdapUserGroupBackendForm extends Form
'label' => $this->translate('Backend Name'), 'label' => $this->translate('Backend Name'),
'description' => $this->translate( 'description' => $this->translate(
'The name of this user group backend that is used to differentiate it from others' 'The name of this user group backend that is used to differentiate it from others'
),
'validators' => array(
array(
'Regex',
false,
array(
'pattern' => '/^[^\\[\\]:]+$/',
'messages' => array(
'regexNotMatch' => $this->translate(
'The name cannot contain \'[\', \']\' or \':\'.'
)
)
)
)
) )
) )
); );

View File

@ -43,7 +43,7 @@ class ConfigForm extends Form
public function save() public function save()
{ {
try { try {
$this->config->saveIni(); $this->writeConfig($this->config);
} catch (Exception $e) { } catch (Exception $e) {
$this->addDecorator('ViewScript', array( $this->addDecorator('ViewScript', array(
'viewModule' => 'default', 'viewModule' => 'default',
@ -58,4 +58,14 @@ class ConfigForm extends Form
return true; return true;
} }
/**
* Write the configuration to disk
*
* @param Config $config
*/
protected function writeConfig(Config $config)
{
$config->saveIni();
}
} }

View File

@ -77,21 +77,7 @@ class DashletForm extends Form
array( array(
'required' => true, 'required' => true,
'label' => $this->translate('Dashlet Title'), 'label' => $this->translate('Dashlet Title'),
'description' => $this->translate('Enter a title for the dashlet.'), 'description' => $this->translate('Enter a title for the dashlet.')
'validators' => array(
array(
'Regex',
false,
array(
'pattern' => '/^[^\\[\\]]+$/',
'messages' => array(
'regexNotMatch' => $this->translate(
'The name cannot contain \'[\' or \']\'.'
)
)
)
)
)
) )
); );
$this->addElement( $this->addElement(

View File

@ -0,0 +1,35 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Navigation;
class DashletForm extends NavigationItemForm
{
/**
* {@inheritdoc}
*/
public function createElements(array $formData)
{
$this->addElement(
'text',
'pane',
array(
'required' => true,
'label' => $this->translate('Pane'),
'description' => $this->translate('The name of the dashboard pane in which to display this dashlet')
)
);
$this->addElement(
'text',
'url',
array(
'required' => true,
'label' => $this->translate('Url'),
'description' => $this->translate(
'The url to load in the dashlet. For external urls, make sure to prepend'
. ' an appropriate protocol identifier (e.g. http://example.tld)'
)
)
);
}
}

View File

@ -0,0 +1,31 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Navigation;
class MenuItemForm extends NavigationItemForm
{
/**
* {@inheritdoc}
*/
protected $requiresParentSelection = true;
/**
* {@inheritdoc}
*/
public function createElements(array $formData)
{
parent::createElements($formData);
// Remove _self and _next as for menu entries only _main is valid
$this->getElement('target')->removeMultiOption('_self');
$this->getElement('target')->removeMultiOption('_next');
$parentElement = $this->getParent()->getElement('parent');
if ($parentElement !== null) {
$parentElement->setDescription($this->translate(
'The parent menu to assign this menu entry to. Select "None" to make this a main menu entry'
));
}
}
}

View File

@ -0,0 +1,824 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Navigation;
use InvalidArgumentException;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Application\Icinga;
use Icinga\Authentication\Auth;
use Icinga\Data\ConfigObject;
use Icinga\Exception\IcingaException;
use Icinga\Exception\NotFoundError;
use Icinga\Exception\ProgrammingError;
use Icinga\Forms\ConfigForm;
use Icinga\User;
use Icinga\Util\String;
use Icinga\Web\Form;
/**
* Form for managing navigation items
*/
class NavigationConfigForm extends ConfigForm
{
/**
* The class namespace where to locate navigation type forms
*
* @var string
*/
const FORM_NS = 'Forms\\Navigation';
/**
* The secondary configuration to write
*
* This is always the reduced configuration and is only written to
* disk once the main configuration has been successfully written.
*
* @var Config
*/
protected $secondaryConfig;
/**
* The navigation item to load when displaying the form for the first time
*
* @var string
*/
protected $itemToLoad;
/**
* The user for whom to manage navigation items
*
* @var User
*/
protected $user;
/**
* The user's navigation configuration
*
* @var Config
*/
protected $userConfig;
/**
* The shared navigation configuration
*
* @var Config
*/
protected $shareConfig;
/**
* The available navigation item types
*
* @var array
*/
protected $itemTypes;
/**
* Initialize this form
*/
public function init()
{
$this->setName('form_config_navigation');
$this->setSubmitLabel($this->translate('Save Changes'));
}
/**
* Set the user for whom to manage navigation items
*
* @param User $user
*
* @return $this
*/
public function setUser(User $user)
{
$this->user = $user;
return $this;
}
/**
* Return the user for whom to manage navigation items
*
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* Set the user's navigation configuration
*
* @param Config $config
*
* @return $this
*/
public function setUserConfig(Config $config)
{
$config->getConfigObject()->setKeyColumn('name');
$this->userConfig = $config;
return $this;
}
/**
* Return the user's navigation configuration
*
* @return Config
*/
public function getUserConfig()
{
if ($this->userConfig === null) {
$this->setUserConfig($this->getUser()->loadNavigationConfig());
}
return $this->userConfig;
}
/**
* Set the shared navigation configuration
*
* @param Config $config
*
* @return $this
*/
public function setShareConfig(Config $config)
{
$config->getConfigObject()->setKeyColumn('name');
$this->shareConfig = $config;
return $this;
}
/**
* Return the shared navigation configuration
*
* @return Config
*/
public function getShareConfig()
{
return $this->shareConfig;
}
/**
* Set the available navigation item types
*
* @param array $itemTypes
*
* @return $this
*/
public function setItemTypes(array $itemTypes)
{
$this->itemTypes = $itemTypes;
return $this;
}
/**
* Return the available navigation item types
*
* @return array
*/
public function getItemTypes()
{
return $this->itemTypes ?: array();
}
/**
* Return a list of available parent items for the given type of navigation item
*
* @param string $type
* @param string $owner
*
* @return array
*/
public function listAvailableParents($type, $owner = null)
{
$children = $this->itemToLoad ? $this->getFlattenedChildren($this->itemToLoad) : array();
$names = array();
foreach ($this->getShareConfig() as $sectionName => $sectionConfig) {
if (
$sectionName !== $this->itemToLoad
&& $sectionConfig->type === $type
&& $sectionConfig->owner === ($owner ?: $this->getUser()->getUsername())
&& !in_array($sectionName, $children, true)
) {
$names[] = $sectionName;
}
}
foreach ($this->getUserConfig() as $sectionName => $sectionConfig) {
if (
$sectionName !== $this->itemToLoad
&& $sectionConfig->type === $type
&& !in_array($sectionName, $children, true)
) {
$names[] = $sectionName;
}
}
return $names;
}
/**
* Recursively return all children of the given navigation item
*
* @param string $name
*
* @return array
*/
protected function getFlattenedChildren($name)
{
$config = $this->getConfigForItem($name);
if ($config === null) {
return array();
}
$children = array();
foreach ($config->toArray() as $sectionName => $sectionConfig) {
if (isset($sectionConfig['parent']) && $sectionConfig['parent'] === $name) {
$children[] = $sectionName;
$children = array_merge($children, $this->getFlattenedChildren($sectionName));
}
}
return $children;
}
/**
* Populate the form with the given navigation item's config
*
* @param string $name
*
* @return $this
*
* @throws NotFoundError In case no navigation item with the given name is found
*/
public function load($name)
{
if ($this->getConfigForItem($name) === null) {
throw new NotFoundError('No navigation item called "%s" found', $name);
}
$this->itemToLoad = $name;
return $this;
}
/**
* Add a new navigation item
*
* The navigation item to add is identified by the array-key `name'.
*
* @param array $data
*
* @return $this
*
* @throws InvalidArgumentException In case $data does not contain a navigation item name
* @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');
}
$shared = false;
$config = $this->getUserConfig();
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();
$shared = true;
} else {
unset($data['users']);
unset($data['groups']);
}
} elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'])) {
$data['owner'] = $this->getUser()->getUsername();
$config = $this->getShareConfig();
$shared = true;
}
$itemName = $data['name'];
$exists = $config->hasSection($itemName);
if (! $exists) {
if ($shared) {
$exists = $this->getUserConfig()->hasSection($itemName);
} else {
$exists = (bool) $this->getShareConfig()
->select()
->where('name', $itemName)
->where('owner', $this->getUser()->getUsername())
->count();
}
}
if ($exists) {
throw new IcingaException(
$this->translate('A navigation item with the name "%s" does already exist'),
$itemName
);
}
unset($data['name']);
$config->setSection($itemName, $data);
$this->setIniConfig($config);
return $this;
}
/**
* Edit a navigation item
*
* @param string $name
* @param array $data
*
* @return $this
*
* @throws NotFoundError In case no navigation item with the given name is found
* @throws IcingaException In case a navigation item with the same name already exists
*/
public function edit($name, array $data)
{
$config = $this->getConfigForItem($name);
if ($config === null) {
throw new NotFoundError('No navigation item called "%s" found', $name);
} else {
$itemConfig = $config->getSection($name);
}
$shared = false;
if ($this->hasBeenShared($name)) {
if (isset($data['parent']) && $data['parent']
? !$this->hasBeenShared($data['parent'])
: ((! isset($data['users']) || !$data['users']) && (! isset($data['groups']) || !$data['groups']))
) {
// It is shared but shouldn't anymore
$config = $this->unshare($name, isset($data['parent']) ? $data['parent'] : null);
}
} elseif ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) {
if ($this->getUser()->can('application/share/navigation')) {
// It is not shared yet but should be
$this->secondaryConfig = $config;
$config = $this->getShareConfig();
$data['owner'] = $this->getUser()->getUsername();
$shared = true;
} else {
unset($data['users']);
unset($data['groups']);
}
} elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'])) {
// Its parent is shared so should it itself
$this->secondaryConfig = $config;
$config = $this->getShareConfig();
$data['owner'] = $this->getUser()->getUsername();
$shared = true;
}
$oldName = null;
if (isset($data['name'])) {
if ($data['name'] !== $name) {
$oldName = $name;
$name = $data['name'];
$exists = $config->hasSection($name);
if (! $exists) {
$ownerName = $itemConfig->owner ?: $this->getUser()->getUsername();
if ($shared || $this->hasBeenShared($oldName)) {
if ($ownerName === $this->getUser()->getUsername()) {
$exists = $this->getUserConfig()->hasSection($name);
} else {
$owner = new User($ownerName);
$exists = $owner->loadNavigationConfig()->hasSection($name);
}
} else {
$exists = (bool) $this->getShareConfig()
->select()
->where('name', $name)
->where('owner', $ownerName)
->count();
}
}
if ($exists) {
throw new IcingaException(
$this->translate('A navigation item with the name "%s" does already exist'),
$name
);
}
}
unset($data['name']);
}
$itemConfig->merge($data);
foreach ($itemConfig->toArray() as $k => $v) {
if ($v === null) {
unset($itemConfig->$k);
}
}
if ($shared) {
// Share all descendant children
foreach ($this->getFlattenedChildren($oldName ?: $name) as $child) {
$childConfig = $this->secondaryConfig->getSection($child);
$this->secondaryConfig->removeSection($child);
$childConfig->owner = $this->getUser()->getUsername();
$config->setSection($child, $childConfig);
}
}
if ($oldName) {
// Update the parent name on all direct children
foreach ($config as $sectionConfig) {
if ($sectionConfig->parent === $oldName) {
$sectionConfig->parent = $name;
}
}
$config->removeSection($oldName);
}
if ($this->secondaryConfig !== null) {
$this->secondaryConfig->removeSection($oldName ?: $name);
}
$config->setSection($name, $itemConfig);
$this->setIniConfig($config);
return $this;
}
/**
* Remove a navigation item
*
* @param string $name
*
* @return ConfigObject The navigation item's config
*
* @throws NotFoundError In case no navigation item with the given name is found
* @throws IcingaException In case the navigation item has still children
*/
public function delete($name)
{
$config = $this->getConfigForItem($name);
if ($config === null) {
throw new NotFoundError('No navigation item called "%s" found', $name);
}
$children = $this->getFlattenedChildren($name);
if (! empty($children)) {
throw new IcingaException(
$this->translate(
'Unable to delete navigation item "%s". There'
. ' are other items dependent from it: %s'
),
$name,
join(', ', $children)
);
}
$section = $config->getSection($name);
$config->removeSection($name);
$this->setIniConfig($config);
return $section;
}
/**
* Unshare the given navigation item
*
* @param string $name
* @param string $parent
*
* @return Config The new config of the given navigation item
*
* @throws NotFoundError In case no navigation item with the given name is found
* @throws IcingaException In case the navigation item has a parent assigned to it
*/
public function unshare($name, $parent = null)
{
$config = $this->getShareConfig();
if (! $config->hasSection($name)) {
throw new NotFoundError('No navigation item called "%s" found', $name);
}
$itemConfig = $config->getSection($name);
if ($parent === null) {
$parent = $itemConfig->parent;
}
if ($parent && $this->hasBeenShared($parent)) {
throw new IcingaException(
$this->translate(
'Unable to unshare navigation item "%s". It is dependent from item "%s".'
. ' Dependent items can only be unshared by unsharing their parent'
),
$name,
$parent
);
}
$children = $this->getFlattenedChildren($name);
$config->removeSection($name);
$this->secondaryConfig = $config;
if (! $itemConfig->owner || $itemConfig->owner === $this->getUser()->getUsername()) {
$config = $this->getUserConfig();
} else {
$owner = new User($itemConfig->owner);
$config = $owner->loadNavigationConfig();
}
foreach ($children as $child) {
$childConfig = $this->secondaryConfig->getSection($child);
unset($childConfig->owner);
$this->secondaryConfig->removeSection($child);
$config->setSection($child, $childConfig);
}
unset($itemConfig->owner);
unset($itemConfig->users);
unset($itemConfig->groups);
$config->setSection($name, $itemConfig);
$this->setIniConfig($config);
return $config;
}
/**
* {@inheritdoc}
*/
public function createElements(array $formData)
{
$shared = false;
$itemTypes = $this->getItemTypes();
$itemType = isset($formData['type']) ? $formData['type'] : key($itemTypes);
$itemForm = $this->getItemForm($itemType);
$this->addElement(
'text',
'name',
array(
'required' => true,
'label' => $this->translate('Name'),
'description' => $this->translate(
'The name of this navigation item that is used to differentiate it from others'
)
)
);
if (
(! $itemForm->requiresParentSelection() || !isset($formData['parent']) || !$formData['parent'])
&& $this->getUser()->can('application/share/navigation')
) {
$checked = isset($formData['shared']) ? null : (isset($formData['users']) || isset($formData['groups']));
$this->addElement(
'checkbox',
'shared',
array(
'autosubmit' => true,
'ignore' => true,
'value' => $checked,
'label' => $this->translate('Shared'),
'description' => $this->translate('Tick this box to share this item with others')
)
);
if ($checked || (isset($formData['shared']) && $formData['shared'])) {
$shared = true;
$this->addElement(
'text',
'users',
array(
'label' => $this->translate('Users'),
'description' => $this->translate(
'Comma separated list of usernames to share this item with'
)
)
);
$this->addElement(
'text',
'groups',
array(
'label' => $this->translate('Groups'),
'description' => $this->translate(
'Comma separated list of group names to share this item with'
)
)
);
}
}
$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)) {
$itemConfig = $this->getShareConfig()->getSection($this->itemToLoad);
$availableParents = $this->listAvailableParents($itemType, $itemConfig->owner);
} else {
$availableParents = $this->listAvailableParents($itemType);
}
$this->addElement(
'select',
'parent',
array(
'allowEmpty' => true,
'autosubmit' => true,
'label' => $this->translate('Parent'),
'description' => $this->translate(
'The parent item to assign this navigation item to. '
. 'Select "None" to make this a main navigation item'
),
'multiOptions' => array_merge(
array('' => $this->translate('None', 'No parent for a navigation item')),
empty($availableParents) ? array() : array_combine($availableParents, $availableParents)
)
)
);
}
$this->addSubForm($itemForm, 'item_form');
$itemForm->create($formData); // May require a parent which gets set by addSubForm()
}
/**
* Populate the configuration of the navigation item to load
*/
public function onRequest()
{
if ($this->itemToLoad) {
$data = $this->getConfigForItem($this->itemToLoad)->getSection($this->itemToLoad)->toArray();
$data['name'] = $this->itemToLoad;
$this->populate($data);
}
}
/**
* {@inheritdoc}
*/
public function isValid($formData)
{
if (! parent::isValid($formData)) {
return false;
}
$valid = true;
if (isset($formData['users']) && $formData['users']) {
$parsedUserRestrictions = array();
foreach (Auth::getInstance()->getRestrictions('application/share/users') as $userRestriction) {
$parsedUserRestrictions[] = array_map('trim', explode(',', $userRestriction));
}
if (! empty($parsedUserRestrictions)) {
$desiredUsers = array_map('trim', explode(',', $formData['users']));
array_unshift($parsedUserRestrictions, $desiredUsers);
$forbiddenUsers = call_user_func_array('array_diff', $parsedUserRestrictions);
if (! empty($forbiddenUsers)) {
$valid = false;
$this->getElement('users')->addError(
$this->translate(sprintf(
'You are not permitted to share this navigation item with the following users: %s',
implode(', ', $forbiddenUsers)
))
);
}
}
}
if (isset($formData['groups']) && $formData['groups']) {
$parsedGroupRestrictions = array();
foreach (Auth::getInstance()->getRestrictions('application/share/groups') as $groupRestriction) {
$parsedGroupRestrictions[] = array_map('trim', explode(',', $groupRestriction));
}
if (! empty($parsedGroupRestrictions)) {
$desiredGroups = array_map('trim', explode(',', $formData['groups']));
array_unshift($parsedGroupRestrictions, $desiredGroups);
$forbiddenGroups = call_user_func_array('array_diff', $parsedGroupRestrictions);
if (! empty($forbiddenGroups)) {
$valid = false;
$this->getElement('groups')->addError(
$this->translate(sprintf(
'You are not permitted to share this navigation item with the following groups: %s',
implode(', ', $forbiddenGroups)
))
);
}
}
}
return $valid;
}
/**
* {@inheritdoc}
*/
public function getValues($suppressArrayNotation = false)
{
$values = parent::getValues();
$values = array_merge($values, $values['item_form']);
unset($values['item_form']);
return $values;
}
/**
* {@inheritdoc}
*/
protected function writeConfig(Config $config)
{
parent::writeConfig($config);
if ($this->secondaryConfig !== null) {
$this->config = $this->secondaryConfig; // Causes the config being displayed to the user in case of an error
parent::writeConfig($this->secondaryConfig);
}
}
/**
* Return the navigation configuration the given item is a part of
*
* @param string $name
*
* @return Config|null In case the item is not part of any configuration
*/
protected function getConfigForItem($name)
{
if ($this->getUserConfig()->hasSection($name)) {
return $this->getUserConfig();
} elseif ($this->getShareConfig()->hasSection($name)) {
if (
$this->getShareConfig()->get($name, 'owner') === $this->getUser()->getUsername()
|| $this->getUser()->can('config/application/navigation')
) {
return $this->getShareConfig();
}
}
}
/**
* Return whether the given navigation item has been shared
*
* @param string $name
*
* @return bool
*/
protected function hasBeenShared($name)
{
return $this->getConfigForItem($name) === $this->getShareConfig();
}
/**
* Return the form for the given type of navigation item
*
* @param string $type
*
* @return Form
*/
protected function getItemForm($type)
{
$className = String::cname($type, '-') . 'Form';
$form = null;
foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
$classPath = 'Icinga\\Module\\'
. ucfirst($module->getName())
. '\\'
. static::FORM_NS
. '\\'
. $className;
if (class_exists($classPath)) {
$form = new $classPath();
break;
}
}
if ($form === null) {
$classPath = 'Icinga\\' . static::FORM_NS . '\\' . $className;
if (class_exists($classPath)) {
$form = new $classPath();
}
}
if ($form === null) {
Logger::debug(
'Failed to find custom navigation item form %s for item %s. Using form NavigationItemForm now',
$className,
$type
);
$form = new NavigationItemForm();
} elseif (! $form instanceof NavigationItemForm) {
throw new ProgrammingError('Class %s must inherit from NavigationItemForm', $classPath);
}
return $form;
}
}

View File

@ -0,0 +1,74 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Navigation;
use Icinga\Web\Form;
class NavigationItemForm extends Form
{
/**
* Whether to create a select input to choose a parent for a navigation item of a particular type
*
* @var bool
*/
protected $requiresParentSelection = false;
/**
* Return whether to create a select input to choose a parent for a navigation item of a particular type
*
* @return bool
*/
public function requiresParentSelection()
{
return $this->requiresParentSelection;
}
/**
* {@inheritdoc}
*/
public function createElements(array $formData)
{
$this->addElement(
'select',
'target',
array(
'allowEmpty' => true,
'label' => $this->translate('Target'),
'description' => $this->translate('The target where to open this navigation item\'s url'),
'multiOptions' => array(
'_blank' => $this->translate('New Window'),
'_next' => $this->translate('New Column'),
'_main' => $this->translate('Single Column'),
'_self' => $this->translate('Current Column')
)
)
);
$this->addElement(
'text',
'url',
array(
'allowEmpty' => true,
'label' => $this->translate('Url'),
'description' => $this->translate(
'The url of this navigation item. Leave blank if you only want the'
. ' name being displayed. For external urls, make sure to prepend'
. ' an appropriate protocol identifier (e.g. http://example.tld)'
)
)
);
$this->addElement(
'text',
'icon',
array(
'allowEmpty' => true,
'label' => $this->translate('Icon'),
'description' => $this->translate(
'The icon of this navigation item. Leave blank if you do not want a icon being displayed'
)
)
);
}
}

View File

@ -28,7 +28,7 @@ class RoleForm extends ConfigForm
* *
* @var array * @var array
*/ */
protected $providedRestrictions = array(); protected $providedRestrictions;
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -37,6 +37,8 @@ class RoleForm extends ConfigForm
{ {
$this->providedPermissions = array( $this->providedPermissions = array(
'*' => $this->translate('Allow everything') . ' (*)', '*' => $this->translate('Allow everything') . ' (*)',
'application/share/navigation' => $this->translate('Allow to share navigation items')
. ' (application/share/navigation)',
'application/stacktraces' => $this->translate( 'application/stacktraces' => $this->translate(
'Allow to adjust in the preferences whether to show stacktraces' 'Allow to adjust in the preferences whether to show stacktraces'
) . ' (application/stacktraces)', ) . ' (application/stacktraces)',
@ -48,6 +50,7 @@ class RoleForm extends ConfigForm
'config/application/resources' => 'config/application/resources', 'config/application/resources' => 'config/application/resources',
'config/application/userbackend' => 'config/application/userbackend', 'config/application/userbackend' => 'config/application/userbackend',
'config/application/usergroupbackend' => 'config/application/usergroupbackend', 'config/application/usergroupbackend' => 'config/application/usergroupbackend',
'config/application/navigation' => 'config/application/navigation',
'config/authentication/*' => 'config/authentication/*', 'config/authentication/*' => 'config/authentication/*',
'config/authentication/users/*' => 'config/authentication/users/*', 'config/authentication/users/*' => 'config/authentication/users/*',
'config/authentication/users/show' => 'config/authentication/users/show', 'config/authentication/users/show' => 'config/authentication/users/show',
@ -67,9 +70,23 @@ class RoleForm extends ConfigForm
'config/modules' => 'config/modules' 'config/modules' => 'config/modules'
*/ */
); );
$helper = new Zend_Form_Element('bogus'); $helper = new Zend_Form_Element('bogus');
$this->providedRestrictions = array(
$helper->filterName('application/share/users') => array(
'name' => 'application/share/users',
'description' => $this->translate(
'Restrict which users this role can share items and information with'
)
),
$helper->filterName('application/share/groups') => array(
'name' => 'application/share/groups',
'description' => $this->translate(
'Restrict which groups this role can share items and information with'
)
)
);
$mm = Icinga::app()->getModuleManager(); $mm = Icinga::app()->getModuleManager();
foreach ($mm->listInstalledModules() as $moduleName) { foreach ($mm->listInstalledModules() as $moduleName) {
$modulePermission = $mm::MODULE_PERMISSION_NS . $moduleName; $modulePermission = $mm::MODULE_PERMISSION_NS . $moduleName;

View File

@ -22,7 +22,7 @@ if ($this->layout()->autorefreshInterval) {
<?php if (Auth::getInstance()->isAuthenticated()): ?> <?php if (Auth::getInstance()->isAuthenticated()): ?>
<?= $this->qlink( <?= $this->qlink(
'', '',
'/dashboard', 'dashboard',
null, null,
array( array(
'icon' => '../logo_icinga-inv.png', 'icon' => '../logo_icinga-inv.png',

View File

@ -63,7 +63,7 @@ $innerLayoutScript = $this->layout()->innerLayout . '.phtml';
<!--<![endif]--> <!--<![endif]-->
<script type="text/javascript"> <script type="text/javascript">
var icinga = new Icinga({ var icinga = new Icinga({
baseUrl: '<?= $this->href('/') ?>' baseUrl: '<?= $this->baseUrl(); ?>'
}); });
</script> </script>
</body> </body>

View File

@ -1,8 +1,6 @@
<?php <?php
use Icinga\Web\Url; use Icinga\Application\Icinga;
use Icinga\Web\Menu;
use Icinga\Web\MenuRenderer;
// Don't render a menu for unauthenticated users unless menu is auth aware // Don't render a menu for unauthenticated users unless menu is auth aware
if (! $this->auth()->isAuthenticated()) { if (! $this->auth()->isAuthenticated()) {
@ -27,10 +25,7 @@ if (! $this->auth()->isAuthenticated()) {
'layout/menu.phtml', 'layout/menu.phtml',
'default', 'default',
array( array(
'menuRenderer' => new MenuRenderer( 'menuRenderer' => Icinga::app()->getMenu()->getRenderer()
Menu::load(),
Url::fromRequest()->without('renderLayout')->getRelativeUrl()
)
) )
) ?> ) ?>
</div> </div>

View File

@ -2,7 +2,7 @@
<?= $tabs; ?> <?= $tabs; ?>
</div> </div>
<div class="content" data-base-target="_next"> <div class="content" data-base-target="_next">
<a href="<?= $this->href('/config/createresource'); ?>"> <a href="<?= $this->href('config/createresource'); ?>">
<?= $this->icon('plus'); ?> <?= $this->translate('Create A New Resource'); ?> <?= $this->icon('plus'); ?> <?= $this->translate('Create A New Resource'); ?>
</a> </a>
<table class="action alternating" id="resource-edit-table"> <table class="action alternating" id="resource-edit-table">

View File

@ -2,7 +2,7 @@
<?= $tabs; ?> <?= $tabs; ?>
</div> </div>
<div class="content" data-base-target="_next"> <div class="content" data-base-target="_next">
<a href="<?= $this->href('/config/createuserbackend'); ?>"> <a href="<?= $this->href('config/createuserbackend'); ?>">
<?= $this->icon('plus'); ?><?= $this->translate('Create A New User Backend'); ?> <?= $this->icon('plus'); ?><?= $this->translate('Create A New User Backend'); ?>
</a> </a>
<div id="authentication-reorder-form"> <div id="authentication-reorder-form">

View File

@ -12,8 +12,5 @@ if ($searchDashboard->search('dummy')->getPane('search')->hasDashlets()): ?>
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
/> />
</form> </form>
<? endif; ?> <?php endif; ?>
<nav> <?= $menuRenderer->setHeading(t('Navigation'))->setElementTag('nav'); ?>
<h1 id="navigation" class="sr-only"><?= t('Navigation'); ?></h1>
<?= $menuRenderer; ?>
</nav>

View File

@ -0,0 +1,6 @@
<div class="controls">
<?= $tabs->showOnlyCloseButton(); ?>
</div>
<div class="content">
<?= $form; ?>
</div>

View File

@ -0,0 +1,53 @@
<?php if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs; ?>
<?= $this->sortBox; ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?= $this->filterEditor; ?>
</div>
<?php endif ?>
<div class="content" data-base-target="_next">
<a href="<?= $this->href('navigation/add'); ?>">
<?= $this->icon('plus'); ?> <?= $this->translate('Create A New Navigation Item'); ?>
</a>
<?php if (count($items) === 0): ?>
<p><?= $this->translate('You did not create any navigation item yet'); ?></p>
<?php else: ?>
<table class="action alternating">
<thead>
<th><?= $this->translate('Navigation'); ?></th>
<th style="width: 10em"><?= $this->translate('Type'); ?></th>
<th style="width: 5em"><?= $this->translate('Shared'); ?></th>
<th style="width: 5em"><?= $this->translate('Remove'); ?></th>
</thead>
<tbody>
<?php foreach ($items as $name => $item): ?>
<tr>
<td><?= $this->qlink(
$name,
'navigation/edit',
array('name' => $name),
array(
'title' => sprintf($this->translate('Edit navigation item %s'), $name)
)
); ?></td>
<td><?= $item->type && isset($types[$item->type])
? $this->escape($types[$item->type])
: $this->escape($this->translate('Unknown')); ?></td>
<td><?= $item->owner ? $this->translate('Yes') : $this->translate('No'); ?></td>
<td><?= $this->qlink(
'',
'navigation/remove',
array('name' => $name),
array(
'icon' => 'trash',
'title' => sprintf($this->translate('Remove navigation item %s'), $name)
)
); ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php endif ?>
</div>

View File

@ -0,0 +1,58 @@
<?php if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs; ?>
<?= $this->sortBox; ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?= $this->filterEditor; ?>
</div>
<?php endif ?>
<div class="content" data-base-target="_next">
<?php if (count($items) === 0): ?>
<p><?= $this->translate('There are currently no navigation items being shared'); ?></p>
<?php else: ?>
<table class="action alternating">
<thead>
<th><?= $this->translate('Shared Navigation'); ?></th>
<th style="width: 10em"><?= $this->translate('Type'); ?></th>
<th style="width: 10em"><?= $this->translate('Owner'); ?></th>
<th style="width: 5em"><?= $this->translate('Unshare'); ?></th>
</thead>
<tbody>
<?php foreach ($items as $name => $item): ?>
<tr>
<td><?= $this->qlink(
$name,
'navigation/edit',
array(
'name' => $name,
'referrer' => 'shared'
),
array(
'title' => sprintf($this->translate('Edit shared navigation item %s'), $name)
)
); ?></td>
<td><?= $item->type && isset($types[$item->type])
? $this->escape($types[$item->type])
: $this->escape($this->translate('Unknown')); ?></td>
<td><?= $this->escape($item->owner); ?></td>
<?php if ($item->parent): ?>
<td><?= $this->icon(
'block',
sprintf(
$this->translate(
'This is a child of the navigation item %1$s. You can'
. ' only unshare this item by unsharing %1$s'
),
$item->parent
)
); ?></td>
<?php else: ?>
<td data-base-target="_self"><?= $removeForm->setDefault('name', $name); ?></td>
<?php endif ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php endif ?>
</div>

View File

@ -301,3 +301,8 @@ The first release candidate of Icinga Web 2 introduces the following non-backwar
* The **instances.ini** configuration file provided by the monitoring module * The **instances.ini** configuration file provided by the monitoring module
has been renamed to **commandtransports.ini**. The content and location of has been renamed to **commandtransports.ini**. The content and location of
the file remains unchanged. the file remains unchanged.
* The location of a user's preferences has been changed from
**<config-dir>/preferences/<username>.ini** to
**<config-dir>/preferences/<username>/config.ini**.
The content of the file remains unchanged.

View File

@ -12,6 +12,7 @@ use Icinga\Data\ConfigObject;
use Icinga\Data\Selectable; use Icinga\Data\Selectable;
use Icinga\Data\SimpleQuery; use Icinga\Data\SimpleQuery;
use Icinga\File\Ini\IniWriter; use Icinga\File\Ini\IniWriter;
use Icinga\File\Ini\IniParser;
use Icinga\Exception\NotReadableError; use Icinga\Exception\NotReadableError;
/** /**
@ -313,9 +314,7 @@ class Config implements Countable, Iterator, Selectable
if ($filepath === false) { if ($filepath === false) {
$emptyConfig->setConfigFile($file); $emptyConfig->setConfigFile($file);
} elseif (is_readable($filepath)) { } elseif (is_readable($filepath)) {
$config = new static(new ConfigObject(parse_ini_file($filepath, true))); return IniParser::parseIniFile($filepath);
$config->setConfigFile($filepath);
return $config;
} elseif (@file_exists($filepath)) { } elseif (@file_exists($filepath)) {
throw new NotReadableError(t('Cannot read config file "%s". Permission denied'), $filepath); throw new NotReadableError(t('Cannot read config file "%s". Permission denied'), $filepath);
} }

View File

@ -0,0 +1,54 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Application\Modules;
/**
* Container for module dashboards
*/
class DashboardContainer extends NavigationItemContainer
{
/**
* This dashboard's dashlets
*
* @var array
*/
protected $dashlets;
/**
* Set this dashboard's dashlets
*
* @param array $dashlets
*
* @return $this
*/
public function setDashlets(array $dashlets)
{
$this->dashlets = $dashlets;
return $this;
}
/**
* Return this dashboard's dashlets
*
* @return array
*/
public function getDashlets()
{
return $this->dashlets ?: array();
}
/**
* Add a new dashlet
*
* @param string $name
* @param string $url
*
* @return $this
*/
public function add($name, $url)
{
$this->dashlets[$name] = $url;
return $this;
}
}

View File

@ -0,0 +1,55 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Application\Modules;
/**
* Container for module menu items
*/
class MenuItemContainer extends NavigationItemContainer
{
/**
* This menu item's children
*
* @var MenuItemContainer[]
*/
protected $children;
/**
* Set this menu item's children
*
* @param MenuItemContainer[] $children
*
* @return $this
*/
public function setChildren(array $children)
{
$this->children = $children;
return $this;
}
/**
* Return this menu item's children
*
* @return array
*/
public function getChildren()
{
return $this->children ?: array();
}
/**
* Add a new sub menu
*
* @param string $name
* @param array $properties
*
* @return MenuItemContainer The newly added sub menu
*/
public function add($name, array $properties = array())
{
$child = new MenuItemContainer($name, $properties);
$this->children[] = $child;
return $child;
}
}

View File

@ -11,7 +11,8 @@ use Icinga\Application\ApplicationBootstrap;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Application\Icinga; use Icinga\Application\Icinga;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Data\ConfigObject; use Icinga\Application\Modules\DashboardContainer;
use Icinga\Application\Modules\MenuItemContainer;
use Icinga\Exception\IcingaException; use Icinga\Exception\IcingaException;
use Icinga\Exception\ProgrammingError; use Icinga\Exception\ProgrammingError;
use Icinga\Module\Setup\SetupWizard; use Icinga\Module\Setup\SetupWizard;
@ -19,9 +20,8 @@ use Icinga\Util\File;
use Icinga\Util\Translator; use Icinga\Util\Translator;
use Icinga\Web\Controller\Dispatcher; use Icinga\Web\Controller\Dispatcher;
use Icinga\Web\Hook; use Icinga\Web\Hook;
use Icinga\Web\Menu; use Icinga\Web\Navigation\Navigation;
use Icinga\Web\Widget; use Icinga\Web\Widget;
use Icinga\Web\Widget\Dashboard\Pane;
/** /**
* Module handling * Module handling
@ -189,7 +189,7 @@ class Module
/** /**
* A set of menu elements * A set of menu elements
* *
* @var Menu[] * @var MenuItemContainer[]
*/ */
protected $menuItems = array(); protected $menuItems = array();
@ -221,6 +221,13 @@ class Module
*/ */
protected $userGroupBackends = array(); protected $userGroupBackends = array();
/**
* This module's configurable navigation items
*
* @var array
*/
protected $navigationItems = array();
/** /**
* Create a new module object * Create a new module object
* *
@ -277,38 +284,98 @@ class Module
} }
/** /**
* Get all pane items * Return this module's dashboard
* *
* @return array * @return Navigation
*/ */
public function getPaneItems() public function getDashboard()
{ {
$this->launchConfigScript(); $this->launchConfigScript();
return $this->paneItems; return $this->createDashboard($this->paneItems);
} }
/** /**
* Add a pane to dashboard * Create and return a new navigation for the given dashboard panes
* *
* @param string $name * @param DashboardContainer[] $panes
* *
* @return Pane * @return Navigation
*/ */
protected function dashboard($name) public function createDashboard(array $panes)
{ {
$this->paneItems[$name] = new Pane($name); $navigation = new Navigation();
foreach ($panes as $pane) {
/** @var DashboardContainer $pane */
$dashlets = array();
foreach ($pane->getDashlets() as $dashletName => $dashletUrl) {
$dashlets[$this->translate($dashletName)] = $dashletUrl;
}
$navigation->addItem(
$pane->getName(),
array_merge(
$pane->getProperties(),
array(
'label' => $this->translate($pane->getName()),
'type' => 'dashboard-pane',
'dashlets' => $dashlets
)
)
);
}
return $navigation;
}
/**
* Add or get a dashboard pane
*
* @param string $name
* @param array $properties
*
* @return DashboardContainer
*/
protected function dashboard($name, array $properties = array())
{
if (array_key_exists($name, $this->paneItems)) {
$this->paneItems[$name]->setProperties($properties);
} else {
$this->paneItems[$name] = new DashboardContainer($name, $properties);
}
return $this->paneItems[$name]; return $this->paneItems[$name];
} }
/** /**
* Get all menu items * Return this module's menu
* *
* @return array * @return Navigation
*/ */
public function getMenuItems() public function getMenu()
{ {
$this->launchConfigScript(); $this->launchConfigScript();
return $this->menuItems; return $this->createMenu($this->menuItems);
}
/**
* Create and return a new navigation for the given menu items
*
* @param MenuItemContainer[] $items
*
* @return Navigation
*/
private function createMenu(array $items)
{
$navigation = new Navigation();
foreach ($items as $item) {
/** @var MenuItemContainer $item */
$navigationItem = $navigation->createItem($item->getName(), $item->getProperties());
$navigationItem->setChildren($this->createMenu($item->getChildren()));
$navigationItem->setLabel($this->translate($item->getName()));
$navigation->addItem($navigationItem);
}
return $navigation;
} }
/** /**
@ -317,14 +384,14 @@ class Module
* @param string $name * @param string $name
* @param array $properties * @param array $properties
* *
* @return Menu * @return MenuItemContainer
*/ */
protected function menuSection($name, array $properties = array()) protected function menuSection($name, array $properties = array())
{ {
if (array_key_exists($name, $this->menuItems)) { if (array_key_exists($name, $this->menuItems)) {
$this->menuItems[$name]->setProperties($properties); $this->menuItems[$name]->setProperties($properties);
} else { } else {
$this->menuItems[$name] = new Menu($name, new ConfigObject($properties)); $this->menuItems[$name] = new MenuItemContainer($name, $properties);
} }
return $this->menuItems[$name]; return $this->menuItems[$name];
@ -831,6 +898,17 @@ class Module
return $this->userGroupBackends; return $this->userGroupBackends;
} }
/**
* Return this module's configurable navigation items
*
* @return array
*/
public function getNavigationItems()
{
$this->launchConfigScript();
return $this->navigationItems;
}
/** /**
* Provide a named permission * Provide a named permission
* *
@ -935,6 +1013,20 @@ class Module
return $this; return $this;
} }
/**
* Provide a new type of configurable navigation item with a optional label
*
* @param string $type
* @param string $label
*
* @return $this
*/
protected function provideNavigationItem($type, $label = null)
{
$this->navigationItems[$type] = $label ?: $type;
return $this;
}
/** /**
* Register module namespaces on our class loader * Register module namespaces on our class loader
* *

View File

@ -0,0 +1,117 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Application\Modules;
use Icinga\Exception\ProgrammingError;
/**
* Container for module navigation items
*/
abstract class NavigationItemContainer
{
/**
* This navigation item's name
*
* @var string
*/
protected $name;
/**
* This navigation item's properties
*
* @var array
*/
protected $properties;
/**
* Create a new NavigationItemContainer
*
* @param string $name
* @param array $properties
*/
public function __construct($name, array $properties = array())
{
$this->name = $name;
$this->properties = $properties;
}
/**
* Set this menu item's name
*
* @param string $name
*
* @return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Return this menu item's name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set this menu item's properties
*
* @param array $properties
*
* @return $this
*/
public function setProperties(array $properties)
{
$this->properties = $properties;
return $this;
}
/**
* Return this menu item's properties
*
* @return array
*/
public function getProperties()
{
return $this->properties ?: array();
}
/**
* Allow dynamic setters and getters for properties
*
* @param string $name
* @param array $arguments
*
* @return mixed
*
* @throws ProgrammingError In case the called method is not supported
*/
public function __call($name, $arguments)
{
if (method_exists($this, $name)) {
return call_user_method_array($name, $this, $arguments);
}
$type = substr($name, 0, 3);
if ($type !== 'set' && $type !== 'get') {
throw new ProgrammingError(
'Dynamic method %s is not supported. Only getters (get*) and setters (set*) are.',
$name
);
}
$propertyName = strtolower(join('_', preg_split('~(?=[A-Z])~', lcfirst(substr($name, 3)))));
if ($type === 'set') {
$this->properties[$propertyName] = $arguments[0];
return $this;
} else { // $type === 'get'
return array_key_exists($propertyName, $this->properties) ? $this->properties[$propertyName] : null;
}
}
}

View File

@ -16,6 +16,7 @@ use Icinga\User;
use Icinga\Util\TimezoneDetect; use Icinga\Util\TimezoneDetect;
use Icinga\Util\Translator; use Icinga\Util\Translator;
use Icinga\Web\Controller\Dispatcher; use Icinga\Web\Controller\Dispatcher;
use Icinga\Web\Navigation\Navigation;
use Icinga\Web\Notification; use Icinga\Web\Notification;
use Icinga\Web\Session; use Icinga\Web\Session;
use Icinga\Web\Session\Session as BaseSession; use Icinga\Web\Session\Session as BaseSession;
@ -139,6 +140,204 @@ class Web extends EmbeddedWeb
return $this->viewRenderer; return $this->viewRenderer;
} }
private function hasAccessToSharedNavigationItem(& $config)
{
// TODO: Provide a more sophisticated solution
if (isset($config['owner']) && $config['owner'] === $this->user->getUsername()) {
unset($config['owner']);
return true;
}
if (isset($config['users'])) {
$users = array_map('trim', explode(',', strtolower($config['users'])));
if (in_array($this->user->getUsername(), $users, true)) {
unset($config['users']);
return true;
}
}
if (isset($config['groups'])) {
$groups = array_map('trim', explode(',', strtolower($config['groups'])));
$userGroups = array_map('strtolower', $this->user->getGroups());
$matches = array_intersect($userGroups, $groups);
if (! empty($matches)) {
unset($config['groups']);
return true;
}
}
return false;
}
/**
* Load and return the shared navigation of the given type
*
* @param string $type
*
* @return Navigation
*/
public function getSharedNavigation($type)
{
$config = Config::app('navigation')->getConfigObject();
$config->setKeyColumn('name');
if ($type === 'dashboard-pane') {
$panes = array();
foreach ($config->select()->where('type', 'dashlet') as $dashletName => $dashletConfig) {
if ($this->hasAccessToSharedNavigationItem($dashletConfig)) {
// TODO: Throw ConfigurationError if pane or url is missing
$panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url;
}
}
$navigation = new Navigation();
foreach ($panes as $paneName => $dashlets) {
$navigation->addItem(
$paneName,
array(
'type' => 'dashboard-pane',
'dashlets' => $dashlets
)
);
}
} else {
$items = array();
foreach ($config->select()->where('type', $type) as $name => $typeConfig) {
if ($this->hasAccessToSharedNavigationItem($typeConfig)) {
$items[$name] = $typeConfig;
}
}
$navigation = Navigation::fromConfig($items);
}
return $navigation;
}
/**
* Return the app's menu
*
* @return Navigation
*/
public function getMenu()
{
if ($this->user !== null) {
$menu = array(
'dashboard' => array(
'label' => t('Dashboard'),
'url' => 'dashboard',
'icon' => 'dashboard',
'priority' => 10
),
'system' => array(
'label' => t('System'),
'icon' => 'services',
'priority' => 700,
'renderer' => array(
'SummaryNavigationItemRenderer',
'state' => 'critical'
),
'children' => array(
'about' => array(
'label' => t('About'),
'url' => 'about',
'priority' => 701
)
)
),
'configuration' => array(
'label' => t('Configuration'),
'icon' => 'wrench',
'permission' => 'config/*',
'priority' => 800,
'children' => array(
'application' => array(
'label' => t('Application'),
'url' => 'config/general',
'permission' => 'config/application/*',
'priority' => 810
),
'navigation' => array(
'label' => t('Shared Navigation'),
'url' => 'navigation/shared',
'permission' => 'config/application/navigation',
'priority' => 820,
),
'authentication' => array(
'label' => t('Authentication'),
'url' => 'config/userbackend',
'permission' => 'config/authentication/*',
'priority' => 830
),
'roles' => array(
'label' => t('Roles'),
'url' => 'role/list',
'permission' => 'config/authentication/roles/show',
'priority' => 840
),
'users' => array(
'label' => t('Users'),
'url' => 'user/list',
'permission' => 'config/authentication/users/show',
'priority' => 850
),
'groups' => array(
'label' => t('Usergroups'),
'url' => 'group/list',
'permission' => 'config/authentication/groups/show',
'priority' => 860
),
'modules' => array(
'label' => t('Modules'),
'url' => 'config/modules',
'permission' => 'config/modules',
'priority' => 890
)
)
),
'user' => array(
'label' => $this->user->getUsername(),
'icon' => 'user',
'priority' => 900,
'children' => array(
'preferences' => array(
'label' => t('Preferences'),
'url' => 'preference',
'priority' => 910
),
'navigation' => array(
'label' => t('Navigation'),
'url' => 'navigation',
'priority' => 920
),
'logout' => array(
'label' => t('Logout'),
'url' => 'authentication/logout',
'priority' => 990,
'renderer' => array(
'NavigationItemRenderer',
'target' => '_self'
)
)
)
)
);
if (Logger::writesToFile()) {
$menu['system']['children']['application_log'] = array(
'label' => t('Application Log'),
'url' => 'list/applicationlog',
'priority' => 710
);
}
} else {
$menu = array();
}
return Navigation::fromArray($menu)->load('menu-item');
}
/** /**
* Dispatch public interface * Dispatch public interface
*/ */

View File

@ -3,6 +3,23 @@
use Icinga\Util\Translator; use Icinga\Util\Translator;
/**
* No-op translate
*
* Supposed to be used for marking a string as available for translation without actually translating it immediately.
* The returned string is the one given in the input. This does only work with the standard gettext macros t() and mt().
*
* @param string $messageId
*
* @return string
*/
function N_($messageId)
{
return $messageId;
}
if (extension_loaded('gettext')) { if (extension_loaded('gettext')) {
/** /**

View File

@ -364,7 +364,12 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
return false; return false;
} }
return $this->ds->testCredentials($userDn, $password); $testCredentialsResult = $this->ds->testCredentials($userDn, $password);
if ($testCredentialsResult) {
$user->setAdditional('ldap_dn', $userDn);
}
return $testCredentialsResult;
} catch (LdapException $e) { } catch (LdapException $e) {
throw new AuthenticationException( throw new AuthenticationException(
'Failed to authenticate user "%s" against backend "%s". An exception was thrown:', 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',

View File

@ -12,6 +12,7 @@ use Icinga\Protocol\Ldap\Expression;
use Icinga\Repository\LdapRepository; use Icinga\Repository\LdapRepository;
use Icinga\Repository\RepositoryQuery; use Icinga\Repository\RepositoryQuery;
use Icinga\User; use Icinga\User;
use Icinga\Application\Logger;
class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBackendInterface class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBackendInterface
{ {
@ -532,18 +533,26 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
*/ */
public function getMemberships(User $user) public function getMemberships(User $user)
{ {
$userQuery = $this->ds if ($this->groupClass === 'posixGroup') {
->select() // Posix group only uses simple user name
->from($this->userClass) $userDn = $user->getUsername();
->where($this->userNameAttribute, $user->getUsername()) } else {
->setBase($this->userBaseDn) // LDAP groups use the complete DN
->setUsePagedResults(false); if (($userDn = $user->getAdditional('ldap_dn')) === null) {
if ($this->userFilter) { $userQuery = $this->ds
$userQuery->where(new Expression($this->userFilter)); ->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) { if (($userDn = $userQuery->fetchDn()) === null) {
return array(); return array();
}
}
} }
$groupQuery = $this->ds $groupQuery = $this->ds
@ -555,10 +564,12 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
$groupQuery->where(new Expression($this->groupFilter)); $groupQuery->where(new Expression($this->groupFilter));
} }
Logger::debug('Fetching groups for user %s using filter %s.', $user->getUsername(), $groupQuery->__toString());
$groups = array(); $groups = array();
foreach ($groupQuery as $row) { foreach ($groupQuery as $row) {
$groups[] = $row->{$this->groupNameAttribute}; $groups[] = $row->{$this->groupNameAttribute};
} }
Logger::debug('Fetched %d groups: %s.', count($groups), join(', ', $groups));
return $groups; return $groups;
} }

View File

@ -97,18 +97,41 @@ class FilterExpression extends Filter
public function matches($row) public function matches($row)
{ {
if (! isset($row->{$this->column})) {
// TODO: REALLY? Exception?
return false;
}
if (is_array($this->expression)) { if (is_array($this->expression)) {
return in_array($row->{$this->column}, $this->expression); return in_array($row->{$this->column}, $this->expression);
} elseif (strpos($this->expression, '*') === false) {
return (string) $row->{$this->column} === (string) $this->expression;
} else {
$parts = preg_split('~\*~', $this->expression);
foreach ($parts as & $part) {
$part = preg_quote($part);
}
$pattern = '/^' . implode('.*', $parts) . '$/';
return (bool) preg_match($pattern, $row->{$this->column});
} }
$expression = (string) $this->expression;
if (strpos($expression, '*') === false) {
if (is_array($row->{$this->column})) {
return in_array($expression, $row->{$this->column});
}
return (string) $row->{$this->column} === $expression;
}
$parts = array();
foreach (preg_split('~\*~', $expression) as $part) {
$parts[] = preg_quote($part);
}
$pattern = '/^' . implode('.*', $parts) . '$/';
if (is_array($row->{$this->column})) {
foreach ($row->{$this->column} as $candidate) {
if (preg_match($pattern, $candidate)) {
return true;
}
}
return false;
}
return (bool) preg_match($pattern, $row->{$this->column});
} }
public function andFilter(Filter $filter) public function andFilter(Filter $filter)

View File

@ -5,21 +5,4 @@ namespace Icinga\Data\Filter;
class FilterMatch extends FilterExpression class FilterMatch extends FilterExpression
{ {
public function matches($row)
{
if (! isset($row->{$this->column})) {
// TODO: REALLY? Exception?
return false;
}
$expression = (string) $this->expression;
if (strpos($expression, '*') === false) {
return (string) $row->{$this->column} === $expression;
} else {
$parts = array();
foreach (preg_split('/\*/', $expression) as $part) {
$parts[] = preg_quote($part);
}
return preg_match('/^' . implode('.*', $parts) . '$/', $row->{$this->column});
}
}
} }

View File

@ -7,15 +7,6 @@ class FilterMatchNot extends FilterExpression
{ {
public function matches($row) public function matches($row)
{ {
$expression = (string) $this->expression; return !parent::matches($row);
if (strpos($expression, '*') === false) {
return (string) $row->{$this->column} !== $expression;
} else {
$parts = array();
foreach (preg_split('/\*/', $expression) as $part) {
$parts[] = preg_quote($part);
}
return ! preg_match('/^' . implode('.*', $parts) . '$/', $row->{$this->column});
}
} }
} }

View File

@ -115,4 +115,18 @@ class Document
} }
return $str; return $str;
} }
/**
* Convert $this to an array
*
* @return array
*/
public function toArray()
{
$a = array();
foreach ($this->sections as $section) {
$a[$section->getName()] = $section->toArray();
}
return $a;
}
} }

View File

@ -169,4 +169,18 @@ class Section
$str = str_replace(';', '\\;', $str); $str = str_replace(';', '\\;', $str);
return str_replace(PHP_EOL, ' ', $str); return str_replace(PHP_EOL, ' ', $str);
} }
/**
* Convert $this to an array
*
* @return array
*/
public function toArray()
{
$a = array();
foreach ($this->directives as $directive) {
$a[$directive->getKey()] = $directive->getValue();
}
return $a;
}
} }

View File

@ -9,6 +9,8 @@ use Icinga\File\Ini\Dom\Document;
use Icinga\File\Ini\Dom\Directive; use Icinga\File\Ini\Dom\Directive;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Exception\ConfigurationError; use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotReadableError;
use Icinga\Application\Config;
class IniParser class IniParser
{ {
@ -239,4 +241,25 @@ class IniParser
} }
return $doc; return $doc;
} }
/**
* Read the ini file and parse it with ::parseIni()
*
* @param string $file The ini file to read
*
* @return Config
* @throws NotReadableError When the file cannot be read
*/
public static function parseIniFile($file)
{
if (($path = realpath($file)) === false) {
throw new NotReadableError('Couldn\'t compute the absolute path of `%s\'', $file);
}
if (($content = file_get_contents($path)) === false) {
throw new NotReadableError('Couldn\'t read the file `%s\'', $path);
}
return Config::fromArray(self::parseIni($content)->toArray())->setConfigFile($file);
}
} }

View File

@ -5,7 +5,9 @@ namespace Icinga;
use DateTimeZone; use DateTimeZone;
use InvalidArgumentException; use InvalidArgumentException;
use Icinga\Application\Config;
use Icinga\User\Preferences; use Icinga\User\Preferences;
use Icinga\Web\Navigation\Navigation;
/** /**
* This class represents an authorized user * This class represents an authorized user
@ -476,4 +478,56 @@ class User
return false; 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
*
* @param string $type
*
* @return Navigation
*/
public function getNavigation($type)
{
$config = $this->loadNavigationConfig();
$config->getConfigObject()->setKeyColumn('name');
if ($type === 'dashboard-pane') {
$panes = array();
foreach ($config->select()->where('type', 'dashlet') as $dashletName => $dashletConfig) {
// TODO: Throw ConfigurationError if pane or url is missing
$panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url;
}
$navigation = new Navigation();
foreach ($panes as $paneName => $dashlets) {
$navigation->addItem(
$paneName,
array(
'type' => 'dashboard-pane',
'dashlets' => $dashlets
)
);
}
} else {
$navigation = Navigation::fromConfig($config->select()->where('type', $type));
}
return $navigation;
}
} }

View File

@ -8,6 +8,7 @@ use Icinga\Exception\NotReadableError;
use Icinga\Exception\NotWritableError; use Icinga\Exception\NotWritableError;
use Icinga\User\Preferences; use Icinga\User\Preferences;
use Icinga\User\Preferences\PreferencesStore; use Icinga\User\Preferences\PreferencesStore;
use Icinga\File\Ini\IniParser;
/** /**
* Load and save user preferences from and to INI files * Load and save user preferences from and to INI files
@ -34,7 +35,7 @@ class IniStore extends PreferencesStore
protected function init() protected function init()
{ {
$this->preferencesFile = sprintf( $this->preferencesFile = sprintf(
'%s/%s.ini', '%s/%s/config.ini',
$this->getStoreConfig()->location, $this->getStoreConfig()->location,
strtolower($this->getUser()->getUsername()) strtolower($this->getUser()->getUsername())
); );
@ -57,7 +58,7 @@ class IniStore extends PreferencesStore
$this->getUser()->getUsername() $this->getUser()->getUsername()
); );
} else { } else {
$this->preferences = parse_ini_file($this->preferencesFile, true); $this->preferences = IniParser::parseIniFile($this->preferencesFile)->toArray();
} }
} }

View File

@ -24,7 +24,7 @@ class String
/** /**
* Uppercase the first character of each word in a string * Uppercase the first character of each word in a string
* *
* Converts 'first_name' to 'firstName' for example. * Converts 'first_name' to 'FirstName' for example.
* *
* @param string $name * @param string $name
* @param string $separator Word separator * @param string $separator Word separator

View File

@ -65,6 +65,15 @@ class Form extends Zend_Form
*/ */
protected $created = false; protected $created = false;
/**
* This form's parent
*
* Gets automatically set upon calling addSubForm().
*
* @var Form
*/
protected $_parent;
/** /**
* Whether the form is an API target * Whether the form is an API target
* *
@ -244,6 +253,29 @@ class Form extends Zend_Form
parent::__construct($options); parent::__construct($options);
} }
/**
* Set this form's parent
*
* @param Form $form
*
* @return $this
*/
public function setParent(Form $form)
{
$this->_parent = $form;
return $this;
}
/**
* Return this form's parent
*
* @return Form
*/
public function getParent()
{
return $this->_parent;
}
/** /**
* Set a callback that is called instead of this form's onSuccess method * Set a callback that is called instead of this form's onSuccess method
* *
@ -845,6 +877,7 @@ class Form extends Zend_Form
$form->setSubmitLabel(''); $form->setSubmitLabel('');
$form->setTokenDisabled(); $form->setTokenDisabled();
$form->setUidDisabled(); $form->setUidDisabled();
$form->setParent($this);
} }
if ($name === null) { if ($name === null) {

View File

@ -0,0 +1,73 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation;
use Icinga\Web\Url;
/**
* A dashboard pane
*/
class DashboardPane extends NavigationItem
{
/**
* This pane's dashlets
*
* @var array
*/
protected $dashlets;
/**
* Set this pane's dashlets
*
* @param array $dashlets
*
* @return $this
*/
public function setDashlets(array $dashlets)
{
$this->dashlets = $dashlets;
return $this;
}
/**
* Return this pane's dashlets
*
* @param bool $ordered Whether to order the dashlets first
*
* @return array
*/
public function getDashlets($ordered = true)
{
if ($this->dashlets === null) {
return array();
}
if ($ordered) {
ksort($this->dashlets);
}
return $this->dashlets;
}
/**
* {@inheritdoc}
*/
public function init()
{
$this->setUrl(Url::fromPath('dashboard', array('pane' => $this->getName())));
}
/**
* {@inheritdoc}
*/
public function merge(NavigationItem $item)
{
parent::merge($item);
$this->setDashlets(array_merge(
$this->getDashlets(false),
$item->getDashlets(false)
));
}
}

View File

@ -5,56 +5,33 @@ namespace Icinga\Web\Navigation;
use ArrayAccess; use ArrayAccess;
use ArrayIterator; use ArrayIterator;
use Exception;
use Countable; use Countable;
use InvalidArgumentException; use InvalidArgumentException;
use IteratorAggregate; use IteratorAggregate;
use Traversable;
use Icinga\Application\Icinga;
use Icinga\Application\Logger;
use Icinga\Authentication\Auth;
use Icinga\Data\ConfigObject;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\IcingaException;
use Icinga\Exception\ProgrammingError;
use Icinga\Util\String;
use Icinga\Web\Navigation\Renderer\RecursiveNavigationRenderer;
/** /**
* Container for navigation items * Container for navigation items
*
* Usage example:
* <code>
* <?php
*
* namespace Icinga\Example;
*
* use Icinga\Web\Navigation\DropdownItem;
* use Icinga\Web\Navigation\Navigation;
* use Icinga\Web\Navigation\NavigationItem;
*
* $navigation = new Navigation();
* $navigation->setLayout(Navigation::LAYOUT_TABS);
* $home = new NavigationItem();
* $home
* ->setIcon('home')
* ->setLabel('Home');
* ->setUrl('/home');
* $logout = new NavigationItem();
* $logout
* ->setIcon('logout')
* ->setLabel('Logout')
* ->setUrl('/logout');
* $dropdown = new DropdownItem();
* $dropdown
* ->setIcon('user')
* ->setLabel('Preferences');
* $preferences = new NavigationItem();
* $preferences
* ->setIcon('preferences');
* ->setLabel('preferences')
* ->setUrl('/preferences');
* $dropdown->addChild($preferences);
* $navigation
* ->addItem($home)
* ->addItem($logout);
* ->addItem($dropdown);
* echo $navigation
* ->getRenderer()
* ->setCssClass('example-nav')
* ->render();
*/ */
class Navigation implements ArrayAccess, Countable, IteratorAggregate class Navigation implements ArrayAccess, Countable, IteratorAggregate
{ {
/**
* The class namespace where to locate navigation type classes
*
* @var string
*/
const NAVIGATION_NS = 'Web\\Navigation';
/** /**
* Flag for dropdown layout * Flag for dropdown layout
* *
@ -70,14 +47,21 @@ class Navigation implements ArrayAccess, Countable, IteratorAggregate
const LAYOUT_TABS = 2; const LAYOUT_TABS = 2;
/** /**
* Navigation items * Known navigation types
*
* @var array
*/
protected static $types;
/**
* This navigation's items
* *
* @var NavigationItem[] * @var NavigationItem[]
*/ */
protected $items = array(); protected $items = array();
/** /**
* Navigation layout * This navigation's layout
* *
* @var int * @var int
*/ */
@ -128,51 +112,141 @@ class Navigation implements ArrayAccess, Countable, IteratorAggregate
*/ */
public function getIterator() public function getIterator()
{ {
$this->order();
return new ArrayIterator($this->items); return new ArrayIterator($this->items);
} }
/** /**
* Ad a navigation item * Create and return a new navigation item for the given configuration
* *
* @param NavigationItem|array $item The item to append * @param string $name
* @param array|ConfigObject $properties
* *
* @return $this * @return NavigationItem
* @throws InvalidArgumentException If the item argument is invalid *
* @throws InvalidArgumentException If the $properties argument is neither an array nor a ConfigObject
*/ */
public function addItem($item) public function createItem($name, $properties)
{ {
if (! $item instanceof NavigationItem) { if ($properties instanceof ConfigObject) {
if (! is_array($item)) { $properties = $properties->toArray();
throw new InvalidArgumentException( } elseif (! is_array($properties)) {
'Argument item must be either an array or an instance of NavigationItem' throw new InvalidArgumentException('Argument $properties must be of type array or ConfigObject');
);
}
$item = new NavigationItem($item);
} }
$this->items[] = $item;
return $this; $itemType = isset($properties['type']) ? String::cname($properties['type'], '-') : 'NavigationItem';
if (! empty(static::$types) && isset(static::$types[$itemType])) {
return new static::$types[$itemType]($name, $properties);
}
$item = null;
foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
$classPath = 'Icinga\\Module\\'
. ucfirst($module->getName())
. '\\'
. static::NAVIGATION_NS
. '\\'
. $itemType;
if (class_exists($classPath)) {
$item = new $classPath($name, $properties);
break;
}
}
if ($item === null) {
$classPath = 'Icinga\\' . static::NAVIGATION_NS . '\\' . $itemType;
if (class_exists($classPath)) {
$item = new $classPath($name, $properties);
}
}
if ($item === null) {
Logger::debug(
'Failed to find custom navigation item class %s for item %s. Using base class NavigationItem now',
$itemType,
$name
);
$item = new NavigationItem($name, $properties);
static::$types[$itemType] = 'Icinga\\Web\\Navigation\\NavigationItem';
} elseif (! $item instanceof NavigationItem) {
throw new ProgrammingError('Class %s must inherit from NavigationItem', $classPath);
} else {
static::$types[$itemType] = $classPath;
}
return $item;
} }
/** /**
* Get the item with the given ID * Add a navigation item
* *
* @param mixed $id * If you do not pass an instance of NavigationItem, this will only add the item
* if it does not require a permission or the current user has the permission.
*
* @param string|NavigationItem $name The name of the item or an instance of NavigationItem
* @param array $properties The properties of the item to add (Ignored if $name is not a string)
*
* @return bool Whether the item was added or not
*
* @throws InvalidArgumentException In case $name is neither a string nor an instance of NavigationItem
*/
public function addItem($name, array $properties = array())
{
if (is_string($name)) {
if (isset($properties['permission'])) {
if (! Auth::getInstance()->hasPermission($properties['permission'])) {
return false;
}
unset($properties['permission']);
}
$item = $this->createItem($name, $properties);
} elseif (! $name instanceof NavigationItem) {
throw new InvalidArgumentException('Argument $name must be of type string or NavigationItem');
} else {
$item = $name;
}
$this->items[$item->getName()] = $item;
return true;
}
/**
* Return the item with the given name
*
* @param string $name
* @param mixed $default * @param mixed $default
* *
* @return NavigationItem|mixed * @return NavigationItem|mixed
*/ */
public function getItem($id, $default = null) public function getItem($name, $default = null)
{ {
if (isset($this->items[$id])) { return isset($this->items[$name]) ? $this->items[$name] : $default;
return $this->items[$id];
}
return $default;
} }
/** /**
* Get the items * Return the currently active item or the first one if none is active
* *
* @return array * @return NavigationItem
*/
public function getActiveItem()
{
foreach ($this->items as $item) {
if ($item->getActive()) {
return $item;
}
}
$firstItem = reset($this->items);
return $firstItem ? $firstItem->setActive() : null;
}
/**
* Return this navigation's items
*
* @return array
*/ */
public function getItems() public function getItems()
{ {
@ -180,19 +254,9 @@ class Navigation implements ArrayAccess, Countable, IteratorAggregate
} }
/** /**
* Get whether the navigation has items * Return whether this navigation is empty
* *
* @return bool * @return bool
*/
public function hasItems()
{
return ! empty($this->items);
}
/**
* Get whether the navigation is empty
*
* @return bool
*/ */
public function isEmpty() public function isEmpty()
{ {
@ -200,9 +264,25 @@ class Navigation implements ArrayAccess, Countable, IteratorAggregate
} }
/** /**
* Get the layout * Return whether this navigation has any renderable items
* *
* @return int * @return bool
*/
public function hasRenderableItems()
{
foreach ($this->getItems() as $item) {
if ($item->shouldRender()) {
return true;
}
}
return false;
}
/**
* Return this navigation's layout
*
* @return int
*/ */
public function getLayout() public function getLayout()
{ {
@ -210,9 +290,9 @@ class Navigation implements ArrayAccess, Countable, IteratorAggregate
} }
/** /**
* Set the layout * Set this navigation's layout
* *
* @param int $layout * @param int $layout
* *
* @return $this * @return $this
*/ */
@ -223,13 +303,250 @@ class Navigation implements ArrayAccess, Countable, IteratorAggregate
} }
/** /**
* Get the navigation renderer * Create and return the renderer for this navigation
* *
* @return RecursiveNavigationRenderer * @return RecursiveNavigationRenderer
*/ */
public function getRenderer() public function getRenderer()
{ {
return new RecursiveNavigationRenderer($this); return new RecursiveNavigationRenderer($this);
} }
}
/**
* Return this navigation rendered to HTML
*
* @return string
*/
public function render()
{
return $this->getRenderer()->render();
}
/**
* Order this navigation's items
*
* @return $this
*/
public function order()
{
uasort($this->items, array($this, 'compareItems'));
foreach ($this->items as $item) {
if ($item->hasChildren()) {
$item->getChildren()->order();
}
}
return $this;
}
/**
* Return whether the first item is less than, more than or equal to the second one
*
* @param NavigationItem $a
* @param NavigationItem $b
*
* @return int
*/
protected function compareItems(NavigationItem $a, NavigationItem $b)
{
if ($a->getPriority() === $b->getPriority()) {
return strcasecmp($a->getLabel(), $b->getLabel());
}
return $a->getPriority() > $b->getPriority() ? 1 : -1;
}
/**
* Try to find and return a item with the given or a similar name
*
* @param string $name
*
* @return NavigationItem
*/
protected function findItem($name)
{
$item = $this->getItem($name);
if ($item !== null) {
return $item;
}
$loweredName = strtolower($name);
foreach ($this->getItems() as $item) {
if (strtolower($item->getName()) === $loweredName) {
return $item;
}
}
}
/**
* Merge this navigation with the given one
*
* Any duplicate items of this navigation will be overwritten by the given navigation's items.
*
* @param Navigation $navigation
*
* @return $this
*/
public function merge(Navigation $navigation)
{
foreach ($navigation as $item) {
/** @var $item NavigationItem */
if (($existingItem = $this->findItem($item->getName())) !== null) {
if ($existingItem->conflictsWith($item)) {
$name = $item->getName();
do {
if (preg_match('~_(\d+)$~', $name, $matches)) {
$name = preg_replace('~_\d+$~', $matches[1] + 1, $name);
} else {
$name .= '_2';
}
} while ($this->getItem($name) !== null);
$this->addItem($item->setName($name));
} else {
$existingItem->merge($item);
}
} else {
$this->addItem($item);
}
}
return $this;
}
/**
* Extend this navigation set with all additional items of the given type
*
* This will fetch navigation items from the following sources:
* * User Shareables
* * User Preferences
* * Modules
* Any existing entry will be overwritten by one that is coming later in order.
*
* @param string $type
*
* @return $this
*/
public function load($type)
{
// Shareables
$this->merge(Icinga::app()->getSharedNavigation($type));
// User Preferences
$user = Auth::getInstance()->getUser();
$this->merge($user->getNavigation($type));
// Modules
$moduleManager = Icinga::app()->getModuleManager();
foreach ($moduleManager->getLoadedModules() as $module) {
if ($user->can($moduleManager::MODULE_PERMISSION_NS . $module->getName())) {
if ($type === 'menu-item') {
$this->merge($module->getMenu());
} elseif ($type === 'dashboard-pane') {
$this->merge($module->getDashboard());
}
}
}
return $this;
}
/**
* Create and return a new set of navigation items for the given configuration
*
* Note that this is supposed to be utilized for one dimensional structures
* only. Multi dimensional structures can be processed by fromArray().
*
* @param Traversable|array $config
*
* @return Navigation
*
* @throws InvalidArgumentException In case the given configuration is invalid
* @throws ConfigurationError In case a referenced parent does not exist
*/
public static function fromConfig($config)
{
if (! is_array($config) && !$config instanceof Traversable) {
throw new InvalidArgumentException('Argument $config must be an array or a instance of Traversable');
}
$flattened = $orphans = $topLevel = array();
foreach ($config as $sectionName => $sectionConfig) {
$parentName = $sectionConfig->parent;
unset($sectionConfig->parent);
if (! $parentName) {
$topLevel[$sectionName] = $sectionConfig->toArray();
$flattened[$sectionName] = & $topLevel[$sectionName];
} elseif (isset($flattened[$parentName])) {
$flattened[$parentName]['children'][$sectionName] = $sectionConfig->toArray();
$flattened[$sectionName] = & $flattened[$parentName]['children'][$sectionName];
} else {
$orphans[$parentName][$sectionName] = $sectionConfig->toArray();
$flattened[$sectionName] = & $orphans[$parentName][$sectionName];
}
}
do {
$match = false;
foreach ($orphans as $parentName => $children) {
if (isset($flattened[$parentName])) {
if (isset($flattened[$parentName]['children'])) {
$flattened[$parentName]['children'] = array_merge(
$flattened[$parentName]['children'],
$children
);
} else {
$flattened[$parentName]['children'] = $children;
}
unset($orphans[$parentName]);
$match = true;
}
}
} while ($match && !empty($orphans));
if (! empty($orphans)) {
throw new ConfigurationError(
t(
'Failed to fully parse navigation configuration. Ensure that'
. ' all referenced parents are existing navigation items: %s'
),
join(', ', array_keys($orphans))
);
}
return static::fromArray($topLevel);
}
/**
* Create and return a new set of navigation items for the given array
*
* @param array $array
*
* @return Navigation
*/
public static function fromArray(array $array)
{
$navigation = new static();
foreach ($array as $name => $properties) {
$navigation->addItem($name, $properties);
}
return $navigation;
}
/**
* Return this navigation rendered to HTML
*
* @return string
*/
public function __toString()
{
try {
return $this->render();
} catch (Exception $e) {
return IcingaException::describe($e);
}
}
}

View File

@ -3,149 +3,188 @@
namespace Icinga\Web\Navigation; namespace Icinga\Web\Navigation;
use Countable; use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use IteratorAggregate; use IteratorAggregate;
use Icinga\Application\Icinga; use Icinga\Application\Icinga;
use Icinga\Web\View; use Icinga\Exception\IcingaException;
use Icinga\Exception\ProgrammingError;
use Icinga\Web\Navigation\Renderer\NavigationItemRenderer;
use Icinga\Web\Url; use Icinga\Web\Url;
/** /**
* A navigation item * A navigation item
*
* @see \Icinga\Web\Navigation\Navigation For a usage example.
*/ */
class NavigationItem implements Countable, IteratorAggregate class NavigationItem implements IteratorAggregate
{ {
/** /**
* Alternative markup element if the navigation item has no URL * Alternative markup element for items without a url
* *
* @var string * @var string
*/ */
const LINK_ALTERNATIVE = 'span'; const LINK_ALTERNATIVE = 'span';
/** /**
* Whether the item is active * The class namespace where to locate navigation type renderer classes
*/
const RENDERER_NS = 'Web\\Navigation\\Renderer';
/**
* Whether this item is active
* *
* @var bool * @var bool
*/ */
protected $active = false; protected $active;
/** /**
* Attributes of the item's element * This item's priority
*
* The priority defines when the item is rendered in relation to its parent's childs.
*
* @var int
*/
protected $priority;
/**
* The attributes of this item's element
* *
* @var array * @var array
*/ */
protected $attributes = array(); protected $attributes;
/** /**
* Item's children * This item's children
* *
* @var Navigation * @var Navigation
*/ */
protected $children; protected $children;
/** /**
* Icon * This item's icon
* *
* @var string|null * @var string
*/ */
protected $icon; protected $icon;
/** /**
* Item's ID * This item's name
* *
* @var mixed * @var string
*/ */
protected $id; protected $name;
/** /**
* Label * This item's label
* *
* @var string|null * @var string
*/ */
protected $label; protected $label;
/** /**
* Parent * This item's parent
* *
* @var NavigationItem|null * @var NavigationItem
*/ */
private $parent; protected $parent;
/** /**
* URL * This item's url
* *
* @var Url|null * @var Url
*/ */
protected $url; protected $url;
/** /**
* URL parameters * This item's url target
*
* @var string
*/
protected $target;
/**
* Additional parameters for this item's url
* *
* @var array * @var array
*/ */
protected $urlParameters = array(); protected $urlParameters;
/** /**
* View * This item's renderer
* *
* @var View|null * @var NavigationItemRenderer
*/ */
protected $view; protected $renderer;
/** /**
* Create a new navigation item * Whether to render this item
* *
* @param array $properties * @var bool
*/ */
public function __construct(array $properties = array()) protected $render;
/**
* Create a new NavigationItem
*
* @param string $name
* @param array $properties
*/
public function __construct($name, array $properties = null)
{ {
$this->setName($name);
$this->priority = 100;
$this->children = new Navigation();
if (! empty($properties)) { if (! empty($properties)) {
$this->setProperties($properties); $this->setProperties($properties);
} }
$this->children = new Navigation();
$this->init(); $this->init();
} }
/** /**
* Initialize the navigation item * Initialize this NavigationItem
*/ */
public function init() public function init()
{ {
} }
/**
* {@inheritdoc}
*/
public function count()
{
return $this->children->count();
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getIterator() public function getIterator()
{ {
return $this->children; return $this->getChildren();
} }
/** /**
* Get whether the item is active * Return whether this item is active
* *
* @return bool * @return bool
*/ */
public function getActive() public function getActive()
{ {
if ($this->active === null) {
$this->active = false;
if ($this->getUrl() !== null && Icinga::app()->getRequest()->getUrl()->matches($this->getUrl())) {
$this->setActive();
} elseif ($this->hasChildren()) {
foreach ($this->getChildren() as $item) {
/** @var NavigationItem $item */
if ($item->getActive()) {
// Do nothing, a true active state is automatically passed to all parents
}
}
}
}
return $this->active; return $this->active;
} }
/** /**
* Set whether the item is active * Set whether this item is active
* *
* Bubbles active state. * If it's active and has a parent, the parent gets activated as well.
* *
* @param bool $active * @param bool $active
* *
@ -154,57 +193,76 @@ class NavigationItem implements Countable, IteratorAggregate
public function setActive($active = true) public function setActive($active = true)
{ {
$this->active = (bool) $active; $this->active = (bool) $active;
$parent = $this; if ($this->active && $this->getParent() !== null) {
while (($parent = $parent->parent) !== null) { $this->getParent()->setActive();
$parent->setActive($active);
} }
return $this; return $this;
} }
/** /**
* Get an attribute's value of the item's element * Return this item's priority
* *
* @param string $name Name of the attribute * @return int
* @param mixed $default Default value */
public function getPriority()
{
return $this->priority;
}
/**
* Set this item's priority
*
* @param int $priority
*
* @return $this
*/
public function setPriority($priority)
{
$this->priority = (int) $priority;
return $this;
}
/**
* Return the value of the given element attribute
*
* @param string $name
* @param mixed $default
* *
* @return mixed * @return mixed
*/ */
public function getAttribute($name, $default = null) public function getAttribute($name, $default = null)
{ {
$name = (string) $name; $attributes = $this->getAttributes();
if (array_key_exists($name, $this->attributes)) { return array_key_exists($name, $attributes) ? $attributes[$name] : $default;
return $this->attributes[$name];
}
return $default;
} }
/** /**
* Set an attribute of the item's element * Set the value of the given element attribute
* *
* @param string $name Name of the attribute * @param string $name
* @param mixed $value Value of the attribute * @param mixed $value
* *
* @return $this * @return $this
*/ */
public function setAttribute($name, $value) public function setAttribute($name, $value)
{ {
$name = (string) $name;
$this->attributes[$name] = $value; $this->attributes[$name] = $value;
return $this; return $this;
} }
/** /**
* Get the item's attributes * Return the attributes of this item's element
* *
* @return array * @return array
*/ */
public function getAttributes() public function getAttributes()
{ {
return $this->attributes; return $this->attributes ?: array();
} }
/** /**
* Set the item's attributes * Set the attributes of this item's element
* *
* @param array $attributes * @param array $attributes
* *
@ -212,44 +270,33 @@ class NavigationItem implements Countable, IteratorAggregate
*/ */
public function setAttributes(array $attributes) public function setAttributes(array $attributes)
{ {
foreach ($attributes as $name => $value) { $this->attributes = $attributes;
$this->setAttribute($name, $value);
}
return $this; return $this;
} }
/** /**
* Add a child item to this item * Add a child to this item
* *
* Bubbles active state. * If the child is active this item gets activated as well.
* *
* @param NavigationItem|array $child The child to add * @param NavigationItem $child
* *
* @return $this * @return $this
* @throws InvalidArgumentException If the child argument is invalid
*/ */
public function addChild($child) public function addChild(NavigationItem $child)
{ {
if (! $child instanceof NavigationItem) { $this->getChildren()->addItem($child->setParent($this));
if (! is_array($child)) {
throw new InvalidArgumentException(
'Argument child must be either an array or an instance of NavigationItem'
);
}
$child = new NavigationItem($child);
}
$child->parent = $this;
$this->children->addItem($child);
if ($child->getActive()) { if ($child->getActive()) {
$this->setActive(); $this->setActive();
} }
return $this; return $this;
} }
/** /**
* Get the item's children * Return this item's children
* *
* @return Navigation * @return Navigation
*/ */
public function getChildren() public function getChildren()
{ {
@ -257,32 +304,42 @@ class NavigationItem implements Countable, IteratorAggregate
} }
/** /**
* Get whether the item has children * Return whether this item has any children
* *
* @return bool * @return bool
*/ */
public function hasChildren() public function hasChildren()
{ {
return ! $this->children->isEmpty(); return !$this->getChildren()->isEmpty();
} }
/** /**
* Set children * Set this item's children
* *
* @param Navigation $children * @param array|Navigation $children
* *
* @return $this * @return $this
*/ */
public function setChildren(Navigation $children) public function setChildren($children)
{ {
if (is_array($children)) {
$children = Navigation::fromArray($children);
} elseif (! $children instanceof Navigation) {
throw new InvalidArgumentException('Argument $children must be of type array or Navigation');
}
foreach ($children as $item) {
$item->setParent($this);
}
$this->children = $children; $this->children = $children;
return $this; return $this;
} }
/** /**
* Get the icon * Return this item's icon
* *
* @return string|null * @return string
*/ */
public function getIcon() public function getIcon()
{ {
@ -290,7 +347,7 @@ class NavigationItem implements Countable, IteratorAggregate
} }
/** /**
* Set the icon * Set this item's icon
* *
* @param string $icon * @param string $icon
* *
@ -298,45 +355,92 @@ class NavigationItem implements Countable, IteratorAggregate
*/ */
public function setIcon($icon) public function setIcon($icon)
{ {
$this->icon = (string) $icon; $this->icon = $icon;
return $this; return $this;
} }
/** /**
* Get the item's ID * Return this item's name escaped with only ASCII chars and/or digits
* *
* @return mixed * @return string
*/ */
public function getId() protected function getEscapedName()
{ {
return $this->id; return preg_replace('~[^a-zA-Z0-9]~', '_', $this->getName());
} }
/** /**
* Set the item's ID * Return a unique version of this item's name
* *
* @param mixed $id ID of the item * @return string
*/
public function getUniqueName()
{
if ($this->getParent() === null) {
return 'navigation-' . $this->getEscapedName();
}
return $this->getParent()->getUniqueName() . '-' . $this->getEscapedName();
}
/**
* Return this item's name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set this item's name
*
* @param string $name
* *
* @return $this * @return $this
*/ */
public function setId($id) public function setName($name)
{ {
$this->id = $id; $this->name = $name;
return $this; return $this;
} }
/** /**
* Get the label * Set this item's parent
* *
* @return string|null * @param NavigationItem $parent
*
* @return $this
*/ */
public function getLabel() public function setParent(NavigationItem $parent)
{ {
return $this->label; $this->parent = $parent;
return $this;
} }
/** /**
* Set the label * Return this item's parent
*
* @return NavigationItem
*/
public function getParent()
{
return $this->parent;
}
/**
* Return this item's label
*
* @return string
*/
public function getLabel()
{
return $this->label ?: $this->getName();
}
/**
* Set this item's label
* *
* @param string $label * @param string $label
* *
@ -344,48 +448,108 @@ class NavigationItem implements Countable, IteratorAggregate
*/ */
public function setLabel($label) public function setLabel($label)
{ {
$this->label = (string) $label; $this->label = $label;
return $this; return $this;
} }
/** /**
* Get the URL * Set this item's url target
* *
* @return Url|null * @param string $target
*
* @return $this
*/
public function setTarget($target)
{
$this->target = $target;
return $this;
}
/**
* Return this item's url target
*
* @return string
*/
public function getTarget()
{
return $this->target;
}
/**
* Return this item's url
*
* @return Url
*/ */
public function getUrl() public function getUrl()
{ {
if ($this->url !== null && ! $this->url instanceof Url) { if ($this->url === null && $this->hasChildren()) {
$this->url = Url::fromPath((string) $this->url); $this->setUrl(Url::fromPath('#'));
} }
return $this->url; return $this->url;
} }
/** /**
* Set the URL * Set this item's url
* *
* @param Url|string $url * @param Url|string $url
* *
* @return $this * @return $this
*
* @throws InvalidArgumentException If the given url is neither of type
*/ */
public function setUrl($url) public function setUrl($url)
{ {
if (is_string($url)) {
$url = Url::fromPath($url);
} elseif (! $url instanceof Url) {
throw new InvalidArgumentException('Argument $url must be of type string or Url');
}
$this->url = $url; $this->url = $url;
return $this; return $this;
} }
/** /**
* Get the URL parameters * Return the value of the given url parameter
* *
* @return array * @param string $name
* @param mixed $default
*
* @return mixed
*/ */
public function getUrlParameters() public function getUrlParameter($name, $default = null)
{ {
return $this->urlParameters; $parameters = $this->getUrlParameters();
return isset($parameters[$name]) ? $parameters[$name] : $default;
} }
/** /**
* Set the URL parameters * Set the value of the given url parameter
*
* @param string $name
* @param mixed $value
*
* @return $this
*/
public function setUrlParameter($name, $value)
{
$this->urlParameters[$name] = $value;
return $this;
}
/**
* Return all additional parameters for this item's url
*
* @return array
*/
public function getUrlParameters()
{
return $this->urlParameters ?: array();
}
/**
* Set additional parameters for this item's url
* *
* @param array $urlParameters * @param array $urlParameters
* *
@ -398,86 +562,230 @@ class NavigationItem implements Countable, IteratorAggregate
} }
/** /**
* Get the view * Set this item's properties
* *
* @return View * Unknown properties (no matching setter) are considered as element attributes.
*/
public function getView()
{
if ($this->view === null) {
$this->view = Icinga::app()->getViewRenderer()->view;
}
return $this->view;
}
/**
* Set the view
*
* @param View $view
*
* @return $this
*/
public function setView(View $view)
{
$this->view = $view;
return $this;
}
/**
* Set properties for the item
* *
* @param array $properties * @param array $properties
* *
* @return $this * @return $this
*/ */
public function setProperties(array $properties = array()) public function setProperties(array $properties)
{ {
foreach ($properties as $name => $value) { foreach ($properties as $name => $value) {
$setter = 'set' . ucfirst($name); $setter = 'set' . ucfirst($name);
if (method_exists($this, $setter)) { if (method_exists($this, $setter)) {
$this->$setter($value); $this->$setter($value);
} else {
$this->setAttribute($name, $value);
} }
} }
return $this; return $this;
} }
/** /**
* Render the navigation item to HTML * Merge this item with the given one
* *
* @return string * @param NavigationItem $item
*
* @return $this
*/ */
public function render() public function merge(NavigationItem $item)
{ {
$view = $this->getView(); if ($this->conflictsWith($item)) {
$label = $view->escape($this->getLabel()); throw new ProgrammingError('Cannot merge, conflict detected.');
if (null !== $icon = $this->getIcon()) {
$label = $view->icon($icon) . $label;
} }
if (null !== $url = $this->getUrl()) {
$content = sprintf( if ($item->getActive()) {
'<a%s href="%s">%s</a>', $this->setActive();
$view->propertiesToString($this->getAttributes()),
$view->url($url, $this->getUrlParameters()),
$label
);
} else {
$content = sprintf(
'<%1$s%2$s>%3$s</%1$s>',
static::LINK_ALTERNATIVE,
$view->propertiesToString($this->getAttributes()),
$label
);
} }
return $content;
if (! $this->getIcon()) {
$this->setIcon($item->getIcon());
}
if ($this->getLabel() === $this->getName()) {
$this->setLabel($item->getLabel());
}
foreach ($item->getAttributes() as $name => $value) {
$this->setAttribute($name, $value);
}
foreach ($item->getUrlParameters() as $name => $value) {
$this->setUrlParameter($name, $value);
}
if ($item->hasChildren()) {
$this->getChildren()->merge($item->getChildren());
}
return $this;
} }
/** /**
* Render the navigation item to HTML * Return whether it's possible to merge this item with the given one
* *
* @return string * @param NavigationItem $item
*
* @return bool
*/
public function conflictsWith(NavigationItem $item)
{
if (! $item instanceof $this) {
return true;
}
if ($this->getUrl() === null || $item->getUrl() === null) {
return false;
}
return !$this->getUrl()->matches($item->getUrl());
}
/**
* Create and return the given renderer
*
* @param string|array $name
*
* @return NavigationItemRenderer
*/
protected function createRenderer($name)
{
if (is_array($name)) {
$options = array_splice($name, 1);
$name = $name[0];
} else {
$options = array();
}
$renderer = null;
foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
$classPath = 'Icinga\\Module\\' . ucfirst($module->getName()) . '\\' . static::RENDERER_NS . '\\' . $name;
if (class_exists($classPath)) {
$renderer = new $classPath($options);
break;
}
}
if ($renderer === null) {
$classPath = 'Icinga\\' . static::RENDERER_NS . '\\' . $name;
if (class_exists($classPath)) {
$renderer = new $classPath($options);
}
}
if ($renderer === null) {
throw new ProgrammingError(
'Cannot find renderer "%s" for navigation item "%s"',
$name,
$this->getName()
);
} elseif (! $renderer instanceof NavigationItemRenderer) {
throw new ProgrammingError('Class %s must inherit from NavigationItemRenderer', $classPath);
}
return $renderer;
}
/**
* Set this item's renderer
*
* @param string|array|NavigationItemRenderer $renderer
*
* @return $this
*
* @throws InvalidArgumentException If the $renderer argument is neither a string nor a NavigationItemRenderer
*/
public function setRenderer($renderer)
{
if (is_string($renderer) || is_array($renderer)) {
$renderer = $this->createRenderer($renderer);
} elseif (! $renderer instanceof NavigationItemRenderer) {
throw new InvalidArgumentException(
'Argument $renderer must be of type string, array or NavigationItemRenderer'
);
}
$this->renderer = $renderer;
return $this;
}
/**
* Return this item's renderer
*
* @return NavigationItemRenderer
*/
public function getRenderer()
{
if ($this->renderer === null) {
$this->setRenderer('NavigationItemRenderer');
}
return $this->renderer;
}
/**
* Set whether this item should be rendered
*
* @param bool $state
*
* @return $this
*/
public function setRender($state = true)
{
$this->render = (bool) $state;
return $this;
}
/**
* Return whether this item should be rendered
*
* @return bool
*/
public function getRender()
{
if ($this->render === null) {
return true;
}
return $this->render;
}
/**
* Return whether this item should be rendered
*
* Alias for NavigationItem::getRender().
*
* @return bool
*/
public function shouldRender()
{
return $this->getRender();
}
/**
* Return this item rendered to HTML
*
* @return string
*/
public function render()
{
return $this->getRenderer()->setItem($this)->render();
}
/**
* Return this item rendered to HTML
*
* @return string
*/ */
public function __toString() public function __toString()
{ {
return $this->render(); try {
return $this->render();
} catch (Exception $e) {
return IcingaException::describe($e);
}
} }
} }

View File

@ -0,0 +1,118 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation\Renderer;
use Icinga\Web\Navigation\NavigationItem;
/**
* Abstract base class for a NavigationItem with a status badge
*/
abstract class BadgeNavigationItemRenderer extends NavigationItemRenderer
{
const STATE_OK = 'ok';
const STATE_CRITICAL = 'critical';
const STATE_WARNING = 'warning';
const STATE_PENDING = 'pending';
const STATE_UNKNOWN = 'unknown';
/**
* The tooltip text for the badge
*
* @var string
*/
protected $title;
/**
* The state identifier being used
*
* The state identifier defines the background color of the badge.
*
* @var string
*/
protected $state;
/**
* Set the tooltip text for the badge
*
* @param string $title
*
* @return $this
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Return the tooltip text for the badge
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Set the state identifier to use
*
* @param string $state
*
* @return $this
*/
public function setState($state)
{
$this->state = $state;
return $this;
}
/**
* Return the state identifier to use
*
* @return string
*/
public function getState()
{
return $this->state;
}
/**
* Return the amount of items represented by the badge
*
* @return int
*/
abstract public function getCount();
/**
* Render the given navigation item as HTML anchor with a badge
*
* @param NavigationItem $item
*
* @return string
*/
public function render(NavigationItem $item = null)
{
return $this->renderBadge() . parent::render($item);
}
/**
* Render the badge
*
* @return string
*/
protected function renderBadge()
{
if (($count = $this->getCount()) > 0) {
return sprintf(
'<div title="%s" class="badge-container"><span class="badge badge-%s">%s</span></div>',
$this->view()->escape($this->getTitle()),
$this->view()->escape($this->getState()),
$count
);
}
return '';
}
}

View File

@ -0,0 +1,197 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation\Renderer;
use Icinga\Application\Icinga;
use Icinga\Exception\ProgrammingError;
use Icinga\Util\String;
use Icinga\Web\Navigation\NavigationItem;
use Icinga\Web\Url;
use Icinga\Web\View;
/**
* NavigationItemRenderer
*/
class NavigationItemRenderer
{
/**
* View
*
* @var View
*/
protected $view;
/**
* The item being rendered
*
* @var NavigationItem
*/
protected $item;
/**
* Internal link targets provided by Icinga Web 2
*
* @var array
*/
protected $internalLinkTargets;
/**
* Create a new NavigationItemRenderer
*
* @param array $options
*/
public function __construct(array $options = null)
{
if (! empty($options)) {
$this->setOptions($options);
}
$this->internalLinkTargets = array('_main', '_self', '_next');
$this->init();
}
/**
* Initialize this renderer
*/
public function init()
{
}
/**
* Set the given options
*
* @param array $options
*
* @return $this
*/
public function setOptions(array $options)
{
foreach ($options as $name => $value) {
$setter = 'set' . String::cname($name);
if (method_exists($this, $setter)) {
$this->$setter($value);
}
}
}
/**
* Set the view
*
* @param View $view
*
* @return $this
*/
public function setView(View $view)
{
$this->view = $view;
return $this;
}
/**
* Return the view
*
* @return View
*/
public function view()
{
if ($this->view === null) {
$this->setView(Icinga::app()->getViewRenderer()->view);
}
return $this->view;
}
/**
* Set the navigation item to render
*
* @param NavigationItem $item
*
* @return $this
*/
public function setItem(NavigationItem $item)
{
$this->item = $item;
return $this;
}
/**
* Return the navigation item being rendered
*
* @return NavigationItem
*/
public function getItem()
{
return $this->item;
}
/**
* Render the given navigation item as HTML anchor
*
* @param NavigationItem $item
*
* @return string
*/
public function render(NavigationItem $item = null)
{
if ($item !== null) {
$this->setItem($item);
} elseif (($item = $this->getItem()) === null) {
throw new ProgrammingError(
'Cannot render nothing. Pass the item to render as part'
. ' of the call to render() or set it with setItem()'
);
}
$label = $this->view()->escape($item->getLabel());
if (($icon = $item->getIcon()) !== null) {
$label = $this->view()->icon($icon) . $label;
}
if (($url = $item->getUrl()) !== null) {
$url->overwriteParams($item->getUrlParameters());
$target = $item->getTarget();
if ($url->isExternal() && (!$target || in_array($target, $this->internalLinkTargets, true))) {
$url = Url::fromPath('iframe', array('url' => $url));
}
$content = sprintf(
'<a%s href="%s"%s>%s</a>',
$this->view()->propertiesToString($item->getAttributes()),
$url,
$this->renderTargetAttribute(),
$label
);
} else {
$content = sprintf(
'<%1$s%2$s>%3$s</%1$s>',
$item::LINK_ALTERNATIVE,
$this->view()->propertiesToString($item->getAttributes()),
$label
);
}
return $content;
}
/**
* Render and return the attribute to provide a non-default target for the url
*
* @return string
*/
protected function renderTargetAttribute()
{
$target = $this->getItem()->getTarget();
if ($target === null) {
return '';
}
if (! in_array($target, $this->internalLinkTargets, true)) {
return ' target="' . $this->view()->escape($target) . '"';
}
return ' data-base-target="' . $target . '"';
}
}

View File

@ -0,0 +1,367 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation\Renderer;
use ArrayIterator;
use Exception;
use RecursiveIterator;
use Icinga\Application\Icinga;
use Icinga\Exception\IcingaException;
use Icinga\Web\Navigation\Navigation;
use Icinga\Web\Navigation\NavigationItem;
use Icinga\Web\View;
/**
* Renderer for single level navigation
*/
class NavigationRenderer implements RecursiveIterator, NavigationRendererInterface
{
/**
* The tag used for the outer element
*
* @var string
*/
protected $elementTag;
/**
* The CSS class used for the outer element
*
* @var string
*/
protected $cssClass;
/**
* The navigation's heading text
*
* @var string
*/
protected $heading;
/**
* The content rendered so far
*
* @var array
*/
protected $content;
/**
* Whether to skip rendering the outer element
*
* @var bool
*/
protected $skipOuterElement;
/**
* The navigation's iterator
*
* @var ArrayIterator
*/
protected $iterator;
/**
* The navigation
*
* @var Navigation
*/
protected $navigation;
/**
* View
*
* @var View
*/
protected $view;
/**
* Create a new NavigationRenderer
*
* @param Navigation $navigation
* @param bool $skipOuterElement
*/
public function __construct(Navigation $navigation, $skipOuterElement = false)
{
$this->skipOuterElement = $skipOuterElement;
$this->iterator = $navigation->getIterator();
$this->navigation = $navigation;
$this->content = array();
}
/**
* {@inheritdoc}
*/
public function setElementTag($tag)
{
$this->elementTag = $tag;
return $this;
}
/**
* {@inheritdoc}
*/
public function getElementTag()
{
return $this->elementTag ?: static::OUTER_ELEMENT_TAG;
}
/**
* {@inheritdoc}
*/
public function setCssClass($class)
{
$this->cssClass = $class;
return $this;
}
/**
* {@inheritdoc}
*/
public function getCssClass()
{
return $this->cssClass;
}
/**
* {@inheritdoc}
*/
public function setHeading($heading)
{
$this->heading = $heading;
return $this;
}
/**
* {@inheritdoc}
*/
public function getHeading()
{
return $this->heading;
}
/**
* Return the view
*
* @return View
*/
public function view()
{
if ($this->view === null) {
$this->setView(Icinga::app()->getViewRenderer()->view);
}
return $this->view;
}
/**
* Set the view
*
* @param View $view
*
* @return $this
*/
public function setView(View $view)
{
$this->view = $view;
return $this;
}
/**
* {@inheritdoc}
*/
public function getChildren()
{
return new static($this->current()->getChildren(), $this->skipOuterElement);
}
/**
* {@inheritdoc}
*/
public function hasChildren()
{
return $this->current()->hasChildren();
}
/**
* {@inheritdoc}
*
* @return NavigationItem
*/
public function current()
{
return $this->iterator->current();
}
/**
* {@inheritdoc}
*/
public function key()
{
return $this->iterator->key();
}
/**
* {@inheritdoc}
*/
public function next()
{
$this->iterator->next();
}
/**
* {@inheritdoc}
*/
public function rewind()
{
$this->iterator->rewind();
if (! $this->skipOuterElement) {
$this->content[] = $this->beginMarkup();
}
}
/**
* {@inheritdoc}
*/
public function valid()
{
$valid = $this->iterator->valid();
if (! $this->skipOuterElement && !$valid) {
$this->content[] = $this->endMarkup();
}
return $valid;
}
/**
* Return the opening markup for the navigation
*
* @return string
*/
public function beginMarkup()
{
$content = array();
$content[] = sprintf(
'<%s%s role="navigation">',
$this->getElementTag(),
$this->getCssClass() !== null ? ' class="' . $this->getCssClass() . '"' : ''
);
if (($heading = $this->getHeading()) !== null) {
$content[] = sprintf(
'<h%1$d id="navigation" class="sr-only" tabindex="-1">%2$s</h%1$d>',
static::HEADING_RANK,
$this->view()->escape($heading)
);
}
$content[] = $this->beginChildrenMarkup();
return join("\n", $content);
}
/**
* Return the closing markup for the navigation
*
* @return string
*/
public function endMarkup()
{
$content = array();
$content[] = $this->endChildrenMarkup();
$content[] = '</' . $this->getElementTag() . '>';
return join("\n", $content);
}
/**
* Return the opening markup for multiple navigation items
*
* @return string
*/
public function beginChildrenMarkup()
{
$cssClass = array(static::CSS_CLASS_NAV);
if ($this->navigation->getLayout() === Navigation::LAYOUT_TABS) {
$cssClass[] = static::CSS_CLASS_NAV_TABS;
} elseif ($this->navigation->getLayout() === Navigation::LAYOUT_DROPDOWN) {
$cssClass[] = static::CSS_CLASS_NAV_DROPDOWN;
}
return '<ul class="' . join(' ', $cssClass) . '">';
}
/**
* Return the closing markup for multiple navigation items
*
* @return string
*/
public function endChildrenMarkup()
{
return '</ul>';
}
/**
* Return the opening markup for the given navigation item
*
* @param NavigationItem $item
*
* @return string
*/
public function beginItemMarkup(NavigationItem $item)
{
$cssClass = array(static::CSS_CLASS_ITEM);
if ($item->hasChildren() && $item->getChildren()->getLayout() === Navigation::LAYOUT_DROPDOWN) {
$cssClass[] = static::CSS_CLASS_DROPDOWN;
$item
->setAttribute('class', static::CSS_CLASS_DROPDOWN_TOGGLE)
->setIcon(static::DROPDOWN_TOGGLE_ICON)
->setUrl('#');
}
if ($item->getActive()) {
$cssClass[] = static::CSS_CLASS_ACTIVE;
}
$content = sprintf(
'<li id="%s" class="%s">',
$this->view()->escape($item->getUniqueName()),
join(' ', $cssClass)
);
return $content;
}
/**
* Return the closing markup for a navigation item
*
* @return string
*/
public function endItemMarkup()
{
return '</li>';
}
/**
* {@inheritdoc}
*/
public function render()
{
foreach ($this as $item) {
/** @var NavigationItem $item */
if ($item->shouldRender()) {
$this->content[] = $this->beginItemMarkup($item);
$this->content[] = $item->render();
$this->content[] = $this->endItemMarkup();
}
}
return join("\n", $this->content);
}
/**
* {@inheritdoc}
*/
public function __toString()
{
try {
return $this->render();
} catch (Exception $e) {
return IcingaException::describe($e);
}
}
}

View File

@ -0,0 +1,135 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation\Renderer;
/**
* Interface for navigation renderers
*/
interface NavigationRendererInterface
{
/**
* CSS class for items
*
* @var string
*/
const CSS_CLASS_ITEM = 'nav-item';
/**
* CSS class for active items
*
* @var string
*/
const CSS_CLASS_ACTIVE = 'active';
/**
* CSS class for dropdown items
*
* @var string
*/
const CSS_CLASS_DROPDOWN = 'dropdown-nav-item';
/**
* CSS class for a dropdown item's trigger
*
* @var string
*/
const CSS_CLASS_DROPDOWN_TOGGLE = 'dropdown-toggle';
/**
* CSS class for the ul element
*
* @var string
*/
const CSS_CLASS_NAV = 'nav';
/**
* CSS class for the ul element with dropdown layout
*
* @var string
*/
const CSS_CLASS_NAV_DROPDOWN = 'dropdown-nav';
/**
* CSS class for the ul element with tabs layout
*
* @var string
*/
const CSS_CLASS_NAV_TABS = 'tab-nav';
/**
* Icon for a dropdown item's trigger
*
* @var string
*/
const DROPDOWN_TOGGLE_ICON = 'menu';
/**
* Default tag for the outer element the navigation will be wrapped with
*
* @var string
*/
const OUTER_ELEMENT_TAG = 'div';
/**
* The heading's rank
*
* @var int
*/
const HEADING_RANK = 1;
/**
* Set the tag for the outer element the navigation is wrapped with
*
* @param string $tag
*
* @return $this
*/
public function setElementTag($tag);
/**
* Return the tag for the outer element the navigation is wrapped with
*
* @return string
*/
public function getElementTag();
/**
* Set the CSS class to use for the outer element
*
* @param string $class
*
* @return $this
*/
public function setCssClass($class);
/**
* Get the CSS class used for the outer element
*
* @return string
*/
public function getCssClass();
/**
* Set the navigation's heading text
*
* @param string $heading
*
* @return $this
*/
public function setHeading($heading);
/**
* Return the navigation's heading text
*
* @return string
*/
public function getHeading();
/**
* Return the navigation rendered to HTML
*
* @return string
*/
public function render();
}

View File

@ -0,0 +1,155 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation\Renderer;
use Exception;
use RecursiveIteratorIterator;
use Icinga\Exception\IcingaException;
use Icinga\Web\Navigation\Navigation;
use Icinga\Web\Navigation\NavigationItem;
/**
* Renderer for multi level navigation
*
* @method NavigationRenderer getInnerIterator() {
* {@inheritdoc}
* }
*/
class RecursiveNavigationRenderer extends RecursiveIteratorIterator implements NavigationRendererInterface
{
/**
* The content rendered so far
*
* @var array
*/
protected $content;
/**
* Create a new RecursiveNavigationRenderer
*
* @param Navigation $navigation
*/
public function __construct(Navigation $navigation)
{
$this->content = array();
parent::__construct(
new NavigationRenderer($navigation, true),
RecursiveIteratorIterator::SELF_FIRST
);
}
/**
* {@inheritdoc}
*/
public function setElementTag($tag)
{
$this->getInnerIterator()->setElementTag($tag);
return $this;
}
/**
* {@inheritdoc}
*/
public function getElementTag()
{
return $this->getInnerIterator()->getElementTag();
}
/**
* {@inheritdoc}
*/
public function setCssClass($class)
{
$this->getInnerIterator()->setCssClass($class);
return $this;
}
/**
* {@inheritdoc}
*/
public function getCssClass()
{
return $this->getInnerIterator()->getCssClass();
}
/**
* {@inheritdoc}
*/
public function setHeading($heading)
{
$this->getInnerIterator()->setHeading($heading);
return $this;
}
/**
* {@inheritdoc}
*/
public function getHeading()
{
return $this->getInnerIterator()->getHeading();
}
/**
* {@inheritdoc}
*/
public function beginIteration()
{
$this->content[] = $this->getInnerIterator()->beginMarkup();
}
/**
* {@inheritdoc}
*/
public function endIteration()
{
$this->content[] = $this->getInnerIterator()->endMarkup();
}
/**
* {@inheritdoc}
*/
public function beginChildren()
{
$this->content[] = $this->getInnerIterator()->beginChildrenMarkup();
}
/**
* {@inheritdoc}
*/
public function endChildren()
{
$this->content[] = $this->getInnerIterator()->endChildrenMarkup();
}
/**
* {@inheritdoc}
*/
public function render()
{
foreach ($this as $item) {
/** @var NavigationItem $item */
if ($item->shouldRender()) {
$this->content[] = $this->getInnerIterator()->beginItemMarkup($item);
$this->content[] = $item->render();
if (! $item->hasChildren()) {
$this->content[] = $this->getInnerIterator()->endItemMarkup();
}
}
}
return join("\n", $this->content);
}
/**
* {@inheritdoc}
*/
public function __toString()
{
try {
return $this->render();
} catch (Exception $e) {
return IcingaException::describe($e);
}
}
}

View File

@ -0,0 +1,46 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation\Renderer;
use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
/**
* Summary badge adding up all badges in the navigation's children that have the same state
*/
class SummaryNavigationItemRenderer extends BadgeNavigationItemRenderer
{
/**
* The title of each summarized child
*
* @var array
*/
protected $titles;
/**
* {@inheritdoc}
*/
public function getCount()
{
$count = 0;
foreach ($this->getItem()->getChildren() as $child) {
$renderer = $child->getRenderer();
if ($renderer instanceof BadgeNavigationItemRenderer) {
if ($renderer->getState() === $this->getState()) {
$this->titles[] = $renderer->getTitle();
$count += $renderer->getCount();
}
}
}
return $count;
}
/**
* {@inheritdoc}
*/
public function getTitle()
{
return !empty($this->titles) ? join(', ', $this->titles) : '';
}
}

View File

@ -13,12 +13,17 @@ use Icinga\Data\Filter\Filter;
* returns Urls reflecting all changes made to the url and to the parameters. * returns Urls reflecting all changes made to the url and to the parameters.
* *
* Direct instantiation is prohibited and should be done either with @see Url::fromRequest() or * Direct instantiation is prohibited and should be done either with @see Url::fromRequest() or
* @see Url::fromUrlString() * @see Url::fromPath()
*
* Currently, protocol, host and port are ignored and will be implemented when required
*/ */
class Url class Url
{ {
/**
* Whether this url points to an external resource
*
* @var bool
*/
protected $external;
/** /**
* An array of all parameters stored in this Url * An array of all parameters stored in this Url
* *
@ -41,12 +46,11 @@ class Url
protected $path = ''; protected $path = '';
/** /**
* The baseUrl that will be appended to @see Url::$path in order to * The baseUrl that will be appended to @see Url::$path
* create an absolute Url
* *
* @var string * @var string
*/ */
protected $baseUrl = '/'; protected $baseUrl = '';
protected function __construct() protected function __construct()
{ {
@ -124,7 +128,7 @@ class Url
$request = self::getRequest(); $request = self::getRequest();
} }
if (!is_string($url)) { if (! is_string($url)) {
throw new ProgrammingError( throw new ProgrammingError(
'url "%s" is not a string', 'url "%s" is not a string',
$url $url
@ -132,17 +136,43 @@ class Url
} }
$urlObject = new Url(); $urlObject = new Url();
$baseUrl = $request->getBaseUrl();
$urlObject->setBaseUrl($baseUrl); if ($url === '#') {
$urlObject->setPath($url);
return $urlObject;
}
$urlParts = parse_url($url); $urlParts = parse_url($url);
if (isset($urlParts['path'])) { if (isset($urlParts['scheme']) && (
if ($baseUrl !== '' && strpos($urlParts['path'], $baseUrl) === 0) { $urlParts['scheme'] !== $request->getScheme()
$urlObject->setPath(substr($urlParts['path'], strlen($baseUrl))); || (isset($urlParts['host']) && $urlParts['host'] !== $request->getServer('SERVER_NAME'))
} else { || (isset($urlParts['port']) && $urlParts['port'] != $request->getServer('SERVER_PORT')))
$urlObject->setPath($urlParts['path']); ) {
} $baseUrl = $urlParts['scheme'] . '://' . $urlParts['host'] . (isset($urlParts['port'])
? (':' . $urlParts['port'])
: '');
$urlObject->setIsExternal();
} else {
$baseUrl = '';
} }
if (isset($urlParts['path'])) {
$urlPath = $urlParts['path'];
if ($urlPath && $urlPath[0] === '/') {
$baseUrl = '';
} elseif (! $baseUrl) {
$baseUrl = $request->getBaseUrl();
}
if ($baseUrl && !$urlObject->isExternal() && strpos($urlPath, $baseUrl) === 0) {
$urlObject->setPath(substr($urlPath, strlen($baseUrl)));
} else {
$urlObject->setPath($urlPath);
}
} elseif (! $baseUrl) {
$baseUrl = $request->getBaseUrl();
}
// TODO: This has been used by former filter implementation, remove it: // TODO: This has been used by former filter implementation, remove it:
if (isset($urlParts['query'])) { if (isset($urlParts['query'])) {
$params = UrlParams::fromQueryString($urlParts['query'])->mergeValues($params); $params = UrlParams::fromQueryString($urlParts['query'])->mergeValues($params);
@ -152,6 +182,7 @@ class Url
$urlObject->setAnchor($urlParts['fragment']); $urlObject->setAnchor($urlParts['fragment']);
} }
$urlObject->setBaseUrl($baseUrl);
$urlObject->setParams($params); $urlObject->setParams($params);
return $urlObject; return $urlObject;
} }
@ -179,19 +210,13 @@ class Url
/** /**
* Overwrite the baseUrl * Overwrite the baseUrl
* *
* If an empty Url is given '/' is used as the base
*
* @param string $baseUrl The url path to use as the Url Base * @param string $baseUrl The url path to use as the Url Base
* *
* @return $this * @return $this
*/ */
public function setBaseUrl($baseUrl) public function setBaseUrl($baseUrl)
{ {
if (($baseUrl = rtrim($baseUrl, '/ ')) === '') { $this->baseUrl = rtrim($baseUrl, '/ ');
$baseUrl = '/';
}
$this->baseUrl = $baseUrl;
return $this; return $this;
} }
@ -231,17 +256,61 @@ class Url
} }
/** /**
* Return the relative url with query parameters as a string * Set whether this url points to an external resource
*
* @param bool $state
*
* @return $this
*/
public function setIsExternal($state = true)
{
$this->external = (bool) $state;
return $this;
}
/**
* Return whether this url points to an external resource
*
* @return bool
*/
public function isExternal()
{
return $this->external;
}
/**
* Return the relative url
* *
* @return string * @return string
*/ */
public function getRelativeUrl($separator = '&') public function getRelativeUrl($separator = '&')
{ {
if ($this->params->isEmpty()) { $path = $this->buildPathQueryAndFragment($separator);
return $this->path . $this->anchor; if ($path && $path[0] === '/') {
} else { return '';
return $this->path . '?' . $this->params->toString($separator) . $this->anchor;
} }
return $path;
}
/**
* Return this url's path with its query parameters and fragment as string
*
* @return string
*/
protected function buildPathQueryAndFragment($querySeparator)
{
$anchor = $this->getAnchor();
if ($anchor) {
$anchor = '#' . $anchor;
}
$query = $this->getQueryString($querySeparator);
if ($query) {
$query = '?' . $query;
}
return $this->getPath() . $query . $anchor;
} }
public function setQueryString($queryString) public function setQueryString($queryString)
@ -250,9 +319,9 @@ class Url
return $this; return $this;
} }
public function getQueryString() public function getQueryString($separator = null)
{ {
return (string) $this->params; return $this->params->toString($separator);
} }
/** /**
@ -262,7 +331,17 @@ class Url
*/ */
public function getAbsoluteUrl($separator = '&') public function getAbsoluteUrl($separator = '&')
{ {
return $this->baseUrl . ($this->baseUrl !== '/' ? '/' : '') . $this->getRelativeUrl($separator); $path = $this->buildPathQueryAndFragment($separator);
if ($path && ($path === '#' || $path[0] === '/')) {
return $path;
}
$baseUrl = $this->getBaseUrl();
if (! $baseUrl) {
$baseUrl = '/';
}
return $baseUrl . ($baseUrl !== '/' && $path ? '/' : '') . $path;
} }
/** /**
@ -380,10 +459,20 @@ class Url
*/ */
public function setAnchor($anchor) public function setAnchor($anchor)
{ {
$this->anchor = '#' . $anchor; $this->anchor = $anchor;
return $this; return $this;
} }
/**
* Return the url anchor-part
*
* @return string The site's anchor string without the '#'
*/
public function getAnchor()
{
return $this->anchor;
}
/** /**
* Remove provided key (if string) or keys (if array of string) from the query parameter array * Remove provided key (if string) or keys (if array of string) from the query parameter array
* *

View File

@ -9,6 +9,8 @@ use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotReadableError; use Icinga\Exception\NotReadableError;
use Icinga\Exception\ProgrammingError; use Icinga\Exception\ProgrammingError;
use Icinga\User; use Icinga\User;
use Icinga\Web\Navigation\DashboardPane;
use Icinga\Web\Navigation\Navigation;
use Icinga\Web\Widget\Dashboard\Pane; use Icinga\Web\Widget\Dashboard\Pane;
use Icinga\Web\Widget\Dashboard\Dashlet as DashboardDashlet; use Icinga\Web\Widget\Dashboard\Dashlet as DashboardDashlet;
use Icinga\Web\Url; use Icinga\Web\Url;
@ -68,16 +70,21 @@ class Dashboard extends AbstractWidget
*/ */
public function load() public function load()
{ {
$manager = Icinga::app()->getModuleManager(); $navigation = new Navigation();
foreach ($manager->getLoadedModules() as $module) { $navigation->load('dashboard-pane');
if ($this->getUser()->can($manager::MODULE_PERMISSION_NS . $module->getName())) {
$this->mergePanes($module->getPaneItems()); $panes = array();
foreach ($navigation as $dashboardPane) {
/** @var DashboardPane $dashboardPane */
$pane = new Pane($dashboardPane->getLabel());
foreach ($dashboardPane->getDashlets() as $title => $url) {
$pane->addDashlet($title, $url);
} }
$panes[] = $pane;
} }
$this->loadUserDashboards(); $this->mergePanes($panes);
return $this; return $this;
} }

View File

@ -57,7 +57,9 @@ class AlertsummaryController extends Controller
$this->view->title = $this->translate('Alert Summary'); $this->view->title = $this->translate('Alert Summary');
$this->view->intervalBox = $this->createIntervalBox(); $this->view->intervalBox = $this->createIntervalBox();
$this->view->recentAlerts = $this->createRecentAlerts(); list($recentAlerts, $recentAlertsUrl) = $this->createRecentAlerts();
$this->view->recentAlerts = $recentAlerts;
$this->view->recentAlertsUrl = $recentAlertsUrl;
$this->view->interval = $this->getInterval(); $this->view->interval = $this->getInterval();
$this->view->defectChart = $this->createDefectImage(); $this->view->defectChart = $this->createDefectImage();
$this->view->healingChart = $this->createHealingChart(); $this->view->healingChart = $this->createHealingChart();
@ -80,6 +82,7 @@ class AlertsummaryController extends Controller
); );
$this->applyRestriction('monitoring/filter/objects', $query); $this->applyRestriction('monitoring/filter/objects', $query);
$this->view->notifications = $query; $this->view->notifications = $query;
$this->view->notificationsUrl = 'monitoring/list/notifications';
$this->setupLimitControl(); $this->setupLimitControl();
$this->setupPaginationControl($this->view->notifications); $this->setupPaginationControl($this->view->notifications);
@ -487,7 +490,7 @@ class AlertsummaryController extends Controller
/** /**
* Top recent alerts * Top recent alerts
* *
* @return mixed * @return array
*/ */
private function createRecentAlerts() private function createRecentAlerts()
{ {
@ -508,7 +511,10 @@ class AlertsummaryController extends Controller
$query->order('notification_start_time', 'desc'); $query->order('notification_start_time', 'desc');
return $query->limit(5); return array(
$query->limit(5),
'monitoring/list/notifications?sort=notification_start_time&dir=desc'
);
} }
/** /**

View File

@ -193,20 +193,6 @@ class BackendConfigForm extends ConfigForm
'label' => $this->translate('Backend Name'), 'label' => $this->translate('Backend Name'),
'description' => $this->translate( 'description' => $this->translate(
'The name of this monitoring backend that is used to differentiate it from others' 'The name of this monitoring backend that is used to differentiate it from others'
),
'validators' => array(
array(
'Regex',
false,
array(
'pattern' => '/^[^\\[\\]:]+$/',
'messages' => array(
'regexNotMatch' => $this->translate(
'The name cannot contain \'[\', \']\' or \':\'.'
)
)
)
)
) )
) )
); );

View File

@ -217,20 +217,6 @@ class TransportConfigForm extends ConfigForm
'label' => $this->translate('Transport Name'), 'label' => $this->translate('Transport Name'),
'description' => $this->translate( 'description' => $this->translate(
'The name of this command transport that is used to differentiate it from others' 'The name of this command transport that is used to differentiate it from others'
),
'validators' => array(
array(
'Regex',
false,
array(
'pattern' => '/^[^\\[\\]:]+$/',
'messages' => array(
'regexNotMatch' => $this->translate(
'The name cannot contain \'[\', \']\' or \':\'.'
)
)
)
)
) )
) )
); );

View File

@ -0,0 +1,75 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Forms\Navigation;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\QueryException;
use Icinga\Forms\Navigation\NavigationItemForm;
class ActionForm extends NavigationItemForm
{
/**
* {@inheritdoc}
*/
public function createElements(array $formData)
{
parent::createElements($formData);
$this->addElement(
'text',
'filter',
array(
'allowEmpty' => true,
'label' => $this->translate('Filter'),
'description' => $this->translate(
'Display this action only for objects matching this filter. Leave it blank'
. ' if you want this action being displayed regardless of the object'
)
)
);
}
/**
* {@inheritdoc}
*/
public function isValid($formData)
{
if (! parent::isValid($formData)) {
return false;
}
if (($filterString = $this->getValue('filter')) !== null) {
$filter = Filter::matchAll();
$filter->setAllowedFilterColumns(array(
'host_name',
'hostgroup_name',
'instance_name',
'service_description',
'servicegroup_name',
function ($c) {
return preg_match('/^_(?:host|service)_/', $c);
}
));
try {
$filter->addFilter(Filter::fromQueryString($filterString));
} catch (QueryException $_) {
$this->getElement('filter')->addError(sprintf(
$this->translate('Invalid filter provided. You can only use the following columns: %s'),
implode(', ', array(
'instance_name',
'host_name',
'hostgroup_name',
'service_description',
'servicegroup_name',
'_(host|service)_<customvar-name>'
))
));
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,8 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Forms\Navigation;
class HostActionForm extends ActionForm
{
}

View File

@ -0,0 +1,8 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Forms\Navigation;
class ServiceActionForm extends ActionForm
{
}

View File

@ -59,8 +59,9 @@
<div class="alertsummary-flex-container"> <div class="alertsummary-flex-container">
<div class="alertsummary-flex"> <div class="alertsummary-flex">
<?= $this->partial('list/notifications.phtml', array( <?= $this->partial('list/notifications.phtml', array(
'notifications' => $this->recentAlerts, 'notifications' => $this->recentAlerts,
'compact' => true 'compact' => true,
'notificationsUrl' => $recentAlertsUrl
)); ?> )); ?>
</div> </div>
</div> </div>
@ -70,8 +71,9 @@
<div class="alertsummary-flex-container"> <div class="alertsummary-flex-container">
<div class="alertsummary-flex"> <div class="alertsummary-flex">
<?= $this->partial('list/notifications.phtml', array( <?= $this->partial('list/notifications.phtml', array(
'notifications' => $this->notifications, 'notifications' => $this->notifications,
'compact' => true 'compact' => true,
'notificationsUrl' => $notificationsUrl
)); ?> )); ?>
</div> </div>
</div> </div>

View File

@ -5,7 +5,7 @@
<div class="content" data-base-target="_next"> <div class="content" data-base-target="_next">
<p> <p>
<a href="<?= $this->href('/monitoring/config/createbackend'); ?>"> <a href="<?= $this->href('monitoring/config/createbackend'); ?>">
<?= $this->icon('plus'); ?> <?= $this->translate('Create New Monitoring Backend'); ?> <?= $this->icon('plus'); ?> <?= $this->translate('Create New Monitoring Backend'); ?>
</a> </a>
</p> </p>
@ -20,7 +20,7 @@
<td> <td>
<?= $this->qlink( <?= $this->qlink(
$backendName, $backendName,
'/monitoring/config/editbackend', 'monitoring/config/editbackend',
array('backend-name' => $backendName), array('backend-name' => $backendName),
array( array(
'icon' => 'edit', 'icon' => 'edit',
@ -35,7 +35,7 @@
<td> <td>
<?= $this->qlink( <?= $this->qlink(
'', '',
'/monitoring/config/removebackend', 'monitoring/config/removebackend',
array('backend-name' => $backendName), array('backend-name' => $backendName),
array( array(
'icon' => 'trash', 'icon' => 'trash',
@ -49,7 +49,7 @@
</table> </table>
<h1><?= $this->translate('Command Transports') ?></h1> <h1><?= $this->translate('Command Transports') ?></h1>
<p> <p>
<a href="<?= $this->href('/monitoring/config/createtransport'); ?>"> <a href="<?= $this->href('monitoring/config/createtransport'); ?>">
<?= $this->icon('plus'); ?> <?= $this->translate('Create New Transport'); ?> <?= $this->icon('plus'); ?> <?= $this->translate('Create New Transport'); ?>
</a> </a>
</p> </p>
@ -64,7 +64,7 @@
<td> <td>
<?= $this->qlink( <?= $this->qlink(
$transportName, $transportName,
'/monitoring/config/edittransport', 'monitoring/config/edittransport',
array('transport' => $transportName), array('transport' => $transportName),
array( array(
'icon' => 'edit', 'icon' => 'edit',
@ -79,7 +79,7 @@
<td> <td>
<?= $this->qlink( <?= $this->qlink(
'', '',
'/monitoring/config/removetransport', 'monitoring/config/removetransport',
array('transport' => $transportName), array('transport' => $transportName),
array( array(
'icon' => 'trash', 'icon' => 'trash',

View File

@ -14,7 +14,7 @@
<div class="content"> <div class="content">
<table data-base-target="_next" <table data-base-target="_next"
class="action comments multiselect" class="action comments multiselect"
data-icinga-multiselect-url="/icingaweb2/monitoring/comments/show" data-icinga-multiselect-url="<?= $this->href('monitoring/comments/show'); ?>"
data-icinga-multiselect-related="<?= $this->href("monitoring/comments") ?>" data-icinga-multiselect-related="<?= $this->href("monitoring/comments") ?>"
data-icinga-multiselect-data="comment_id"> data-icinga-multiselect-data="comment_id">
<tbody> <tbody>

View File

@ -27,7 +27,7 @@ if (count($groupData) === 0) {
<div class="box contents"> <div class="box contents">
<?php foreach ($groupInfo['contacts'] as $c): ?> <?php foreach ($groupInfo['contacts'] as $c): ?>
<div class="box entry"> <div class="box entry">
<?= $this->img('/static/gravatar', array('email' => $c->contact_email)); ?> <?= $this->img('static/gravatar', array('email' => $c->contact_email)); ?>
<?= $this->qlink( <?= $this->qlink(
$c->contact_alias, $c->contact_alias,
'monitoring/show/contact', 'monitoring/show/contact',

View File

@ -10,7 +10,7 @@
<div data-base-target="_next" class="content contacts"> <div data-base-target="_next" class="content contacts">
<?php foreach ($contacts->peekAhead($this->compact) as $contact): ?> <?php foreach ($contacts->peekAhead($this->compact) as $contact): ?>
<div class="contact"> <div class="contact">
<?= $this->img('/static/gravatar', array('email' => $contact->contact_email)); ?> <?= $this->img('static/gravatar', array('email' => $contact->contact_email)); ?>
<strong><?= $this->qlink( <strong><?= $this->qlink(
$contact->contact_name, $contact->contact_name,
'monitoring/show/contact', 'monitoring/show/contact',

View File

@ -20,7 +20,7 @@ if (! $this->compact): ?>
<div class="content"> <div class="content">
<table data-base-target="_next" <table data-base-target="_next"
class="action multiselect" class="action multiselect"
data-icinga-multiselect-url="/icingaweb2/monitoring/downtimes/show" data-icinga-multiselect-url="<?= $this->href('monitoring/downtimes/show'); ?>"
data-icinga-multiselect-controllers="<?= $this->href("monitoring/downtimes") ?>" data-icinga-multiselect-controllers="<?= $this->href("monitoring/downtimes") ?>"
data-icinga-multiselect-data="downtime_id"> data-icinga-multiselect-data="downtime_id">
<tbody> <tbody>

View File

@ -72,7 +72,7 @@ if (! $this->compact): ?>
<?php elseif ($notifications->hasMore()): ?> <?php elseif ($notifications->hasMore()): ?>
<?= $this->qlink( <?= $this->qlink(
$this->translate('Show More'), $this->translate('Show More'),
$this->url()->without(array('view', 'limit')), $this->url(isset($notificationsUrl) ? $notificationsUrl : null)->without(array('view', 'limit')),
null, null,
array( array(
'data-base-target' => '_next', 'data-base-target' => '_next',

View File

@ -1,25 +1,38 @@
<?php <?php
use Icinga\Web\Navigation\Navigation;
$navigation = new Navigation();
$navigation->load($object->getType() . '-action');
foreach ($navigation as $item) {
$item->setObject($object);
}
// add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201 // add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201
$newTabInfo = sprintf('<span class="info-box display-on-hover"> %s </span>', $this->translate('opens in new window')); $newTabInfo = sprintf('<span class="info-box display-on-hover"> %s </span>', $this->translate('opens in new window'));
$links = $object->getActionUrls(); foreach ($object->getActionUrls() as $i => $link) {
foreach ($links as $i => $link) { $navigation->addItem(
$links[$i] = sprintf('<a href="%s" target="_blank">%s ' . $newTabInfo . '</a>', $link, 'Action'); 'Action ' . ($i + 1) . $newTabInfo,
array(
'url' => $link,
'target' => '_blank'
)
);
} }
if (isset($this->actions)) { if (isset($this->actions)) {
foreach ($this->actions as $id => $action) { foreach ($this->actions as $id => $action) {
$links[] = sprintf('<a href="%s">%s</a>', $action, $id); $navigation->addItem($id, array('url' => $action));
} }
} }
if (empty($links)) { if ($navigation->isEmpty() || !$navigation->hasRenderableItems()) {
return; return;
} }
?> ?>
<tr> <tr>
<th><?= $this->translate('Actions') ?></th> <th><?= $this->translate('Actions'); ?></th>
<td><?= implode("<br>", $links) ?></td> <?= $navigation->getRenderer()->setElementTag('td')->setCssClass('actions'); ?>
</tr> </tr>

View File

@ -1,26 +1,42 @@
<?php <?php
$notes = trim($object->getNotes());
$links = $object->getNotesUrls();
if (! empty($links) || ! empty($notes)): ?> use Icinga\Web\Navigation\Navigation;
$navigation = new Navigation();
$navigation->load($object->getType() . '-note');
foreach ($navigation as $item) {
$item->setObject($object);
}
$notes = trim($object->getNotes());
if ($notes) {
$navigation->addItem($notes);
}
$links = $object->getNotesUrls();
if (! empty($links)) {
// add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201
$newTabInfo = sprintf(
'<span class="info-box display-on-hover"> %s </span>',
$this->translate('opens in new window')
);
foreach ($links as $link) {
$navigation->addItem(
$this->escape($link) . $newTabInfo,
array(
'url' => $link,
'target' => '_blank'
)
);
}
}
if ($navigation->isEmpty() || !$navigation->hasRenderableItems()) {
return;
}
?>
<tr> <tr>
<th><?= $this->translate('Notes') ?></th> <th><?= $this->translate('Notes'); ?></th>
<td> <?= $navigation->getRenderer()->setElementTag('td')->setCssClass('notes'); ?>
<?php </tr>
if (! empty($notes)) {
echo $notes . '<br>';
}
// add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201
$newTabInfo = sprintf(
'<span class="info-box display-on-hover"> %s </span>',
$this->translate('opens in new window')
);
$linkText = '<a href="%s" target="_blank">%s ' . $newTabInfo . '</a>';
foreach ($links as $i => $link) {
$links[$i] = sprintf($linkText, $this->escape($link), $this->escape($link));
}
echo implode('<br>', $links);
?>
</td>
</tr>
<?php endif ?>

View File

@ -65,7 +65,7 @@ if (! $beingExtended && !$this->compact): ?>
<div class="timeframe"> <div class="timeframe">
<span><?= $this->qlink( <span><?= $this->qlink(
$timeInfo[0]->end->format($intervalFormat), $timeInfo[0]->end->format($intervalFormat),
'/monitoring/list/eventhistory', 'monitoring/list/eventhistory',
array( array(
'timestamp<' => $timeInfo[0]->start->getTimestamp(), 'timestamp<' => $timeInfo[0]->start->getTimestamp(),
'timestamp>' => $timeInfo[0]->end->getTimestamp() 'timestamp>' => $timeInfo[0]->end->getTimestamp()

View File

@ -85,20 +85,29 @@ $this->provideSearchUrl($this->translate('Services'), 'monitoring/list/services?
$this->provideSearchUrl($this->translate('Hostgroups'), 'monitoring/list/hostgroups?limit=10', 97); $this->provideSearchUrl($this->translate('Hostgroups'), 'monitoring/list/hostgroups?limit=10', 97);
$this->provideSearchUrl($this->translate('Servicegroups'), 'monitoring/list/servicegroups?limit=10', 96); $this->provideSearchUrl($this->translate('Servicegroups'), 'monitoring/list/servicegroups?limit=10', 96);
/*
* Available navigation items
*/
$this->provideNavigationItem('host-action', $this->translate('Host Action'));
$this->provideNavigationItem('service-action', $this->translate('Service Action'));
// Notes are disabled as we're not sure whether to really make a difference between actions and notes
//$this->provideNavigationItem('host-note', $this->translate('Host Note'));
//$this->provideNavigationItem('service-note', $this->translate('Service Note'));
/* /*
* Problems Section * Problems Section
*/ */
$section = $this->menuSection($this->translate('Problems'), array( $section = $this->menuSection(N_('Problems'), array(
'renderer' => array( 'renderer' => array(
'SummaryMenuItemRenderer', 'SummaryNavigationItemRenderer',
'state' => 'critical' 'state' => 'critical'
), ),
'icon' => 'block', 'icon' => 'block',
'priority' => 20 'priority' => 20
)); ));
$section->add($this->translate('Unhandled Hosts'), array( $section->add(N_('Unhandled Hosts'), array(
'renderer' => array( 'renderer' => array(
'Icinga\Module\Monitoring\Web\Menu\MonitoringBadgeMenuItemRenderer', 'MonitoringBadgeNavigationItemRenderer',
'columns' => array( 'columns' => array(
'hosts_down_unhandled' => $this->translate('%d unhandled hosts down') 'hosts_down_unhandled' => $this->translate('%d unhandled hosts down')
), ),
@ -108,9 +117,9 @@ $section->add($this->translate('Unhandled Hosts'), array(
'url' => 'monitoring/list/hosts?host_problem=1&host_handled=0', 'url' => 'monitoring/list/hosts?host_problem=1&host_handled=0',
'priority' => 30 'priority' => 30
)); ));
$section->add($this->translate('Unhandled Services'), array( $section->add(N_('Unhandled Services'), array(
'renderer' => array( 'renderer' => array(
'Icinga\Module\Monitoring\Web\Menu\MonitoringBadgeMenuItemRenderer', 'MonitoringBadgeNavigationItemRenderer',
'columns' => array( 'columns' => array(
'services_critical_unhandled' => $this->translate('%d unhandled services critical') 'services_critical_unhandled' => $this->translate('%d unhandled services critical')
), ),
@ -120,19 +129,19 @@ $section->add($this->translate('Unhandled Services'), array(
'url' => 'monitoring/list/services?service_problem=1&service_handled=0&sort=service_severity', 'url' => 'monitoring/list/services?service_problem=1&service_handled=0&sort=service_severity',
'priority' => 40 'priority' => 40
)); ));
$section->add($this->translate('Host Problems'), array( $section->add(N_('Host Problems'), array(
'url' => 'monitoring/list/hosts?host_problem=1&sort=host_severity', 'url' => 'monitoring/list/hosts?host_problem=1&sort=host_severity',
'priority' => 50 'priority' => 50
)); ));
$section->add($this->translate('Service Problems'), array( $section->add(N_('Service Problems'), array(
'url' => 'monitoring/list/services?service_problem=1&sort=service_severity&dir=desc', 'url' => 'monitoring/list/services?service_problem=1&sort=service_severity&dir=desc',
'priority' => 60 'priority' => 60
)); ));
$section->add($this->translate('Service Grid'), array( $section->add(N_('Service Grid'), array(
'url' => 'monitoring/list/servicegrid?problems', 'url' => 'monitoring/list/servicegrid?problems',
'priority' => 70 'priority' => 70
)); ));
$section->add($this->translate('Current Downtimes'), array( $section->add(N_('Current Downtimes'), array(
'url' => 'monitoring/list/downtimes?downtime_is_in_effect=1', 'url' => 'monitoring/list/downtimes?downtime_is_in_effect=1',
'priority' => 80 'priority' => 80
)); ));
@ -140,43 +149,43 @@ $section->add($this->translate('Current Downtimes'), array(
/* /*
* Overview Section * Overview Section
*/ */
$section = $this->menuSection($this->translate('Overview'), array( $section = $this->menuSection(N_('Overview'), array(
'icon' => 'sitemap', 'icon' => 'sitemap',
'priority' => 30 'priority' => 30
)); ));
$section->add($this->translate('Tactical Overview'), array( $section->add(N_('Tactical Overview'), array(
'url' => 'monitoring/tactical', 'url' => 'monitoring/tactical',
'priority' => 40 'priority' => 40
)); ));
$section->add($this->translate('Hosts'), array( $section->add(N_('Hosts'), array(
'url' => 'monitoring/list/hosts', 'url' => 'monitoring/list/hosts',
'priority' => 50 'priority' => 50
)); ));
$section->add($this->translate('Services'), array( $section->add(N_('Services'), array(
'url' => 'monitoring/list/services', 'url' => 'monitoring/list/services',
'priority' => 50 'priority' => 50
)); ));
$section->add($this->translate('Servicegroups'), array( $section->add(N_('Servicegroups'), array(
'url' => 'monitoring/list/servicegroups', 'url' => 'monitoring/list/servicegroups',
'priority' => 60 'priority' => 60
)); ));
$section->add($this->translate('Hostgroups'), array( $section->add(N_('Hostgroups'), array(
'url' => 'monitoring/list/hostgroups', 'url' => 'monitoring/list/hostgroups',
'priority' => 60 'priority' => 60
)); ));
$section->add($this->translate('Contacts'), array( $section->add(N_('Contacts'), array(
'url' => 'monitoring/list/contacts', 'url' => 'monitoring/list/contacts',
'priority' => 70 'priority' => 70
)); ));
$section->add($this->translate('Contactgroups'), array( $section->add(N_('Contactgroups'), array(
'url' => 'monitoring/list/contactgroups', 'url' => 'monitoring/list/contactgroups',
'priority' => 70 'priority' => 70
)); ));
$section->add($this->translate('Comments'), array( $section->add(N_('Comments'), array(
'url' => 'monitoring/list/comments?comment_type=(comment|ack)', 'url' => 'monitoring/list/comments?comment_type=(comment|ack)',
'priority' => 80 'priority' => 80
)); ));
$section->add($this->translate('Downtimes'), array( $section->add(N_('Downtimes'), array(
'url' => 'monitoring/list/downtimes', 'url' => 'monitoring/list/downtimes',
'priority' => 80 'priority' => 80
)); ));
@ -184,22 +193,23 @@ $section->add($this->translate('Downtimes'), array(
/* /*
* History Section * History Section
*/ */
$section = $this->menuSection($this->translate('History'), array( $section = $this->menuSection(N_('History'), array(
'icon' => 'rewind' 'icon' => 'rewind',
'priority' => 90
)); ));
$section->add($this->translate('Event Grid'), array( $section->add(N_('Event Grid'), array(
'priority' => 10, 'priority' => 10,
'url' => 'monitoring/list/eventgrid' 'url' => 'monitoring/list/eventgrid'
)); ));
$section->add($this->translate('Event Overview'), array( $section->add(N_('Event Overview'), array(
'priority' => 20, 'priority' => 20,
'url' => 'monitoring/list/eventhistory?timestamp>=-7%20days' 'url' => 'monitoring/list/eventhistory?timestamp>=-7%20days'
)); ));
$section->add($this->translate('Notifications'), array( $section->add(N_('Notifications'), array(
'priority' => 30, 'priority' => 30,
'url' => 'monitoring/list/notifications', 'url' => 'monitoring/list/notifications',
)); ));
$section->add($this->translate('Timeline'), array( $section->add(N_('Timeline'), array(
'priority' => 40, 'priority' => 40,
'url' => 'monitoring/timeline' 'url' => 'monitoring/timeline'
)); ));
@ -207,144 +217,144 @@ $section->add($this->translate('Timeline'), array(
/* /*
* Reporting Section * Reporting Section
*/ */
$section = $this->menuSection($this->translate('Reporting'), array( $section = $this->menuSection(N_('Reporting'), array(
'icon' => 'barchart', 'icon' => 'barchart',
'priority' => 100 'priority' => 100
)); ));
$section->add($this->translate('Alert Summary'), array( $section->add(N_('Alert Summary'), array(
'url' => 'monitoring/alertsummary/index' 'url' => 'monitoring/alertsummary/index'
)); ));
/* /*
* System Section * System Section
*/ */
$section = $this->menuSection($this->translate('System')); $section = $this->menuSection(N_('System'));
$section->add($this->translate('Monitoring Health'), array( $section->add(N_('Monitoring Health'), array(
'url' => 'monitoring/health/info', 'url' => 'monitoring/health/info',
'priority' => 720, 'priority' => 720,
'renderer' => 'Icinga\Module\Monitoring\Web\Menu\BackendAvailabilityMenuItemRenderer' 'renderer' => 'BackendAvailabilityNavigationItemRenderer'
)); ));
/* /*
* Current Incidents * Current Incidents
*/ */
$dashboard = $this->dashboard($this->translate('Current Incidents')); $dashboard = $this->dashboard(N_('Current Incidents'), array('priority' => 50));
$dashboard->add( $dashboard->add(
$this->translate('Service Problems'), N_('Service Problems'),
'monitoring/list/services?service_problem=1&limit=10&sort=service_severity' 'monitoring/list/services?service_problem=1&limit=10&sort=service_severity'
); );
$dashboard->add( $dashboard->add(
$this->translate('Recently Recovered Services'), N_('Recently Recovered Services'),
'monitoring/list/services?service_state=0&limit=10&sort=service_last_state_change&dir=desc' 'monitoring/list/services?service_state=0&limit=10&sort=service_last_state_change&dir=desc'
); );
$dashboard->add( $dashboard->add(
$this->translate('Host Problems'), N_('Host Problems'),
'monitoring/list/hosts?host_problem=1&sort=host_severity' 'monitoring/list/hosts?host_problem=1&sort=host_severity'
); );
/* /*
* Overview * Overview
*/ */
$dashboard = $this->dashboard($this->translate('Overview')); $dashboard = $this->dashboard(N_('Overview'), array('priority' => 60));
$dashboard->add( $dashboard->add(
$this->translate('Service Grid'), N_('Service Grid'),
'monitoring/list/servicegrid?limit=15,18' 'monitoring/list/servicegrid?limit=15,18'
); );
$dashboard->add( $dashboard->add(
$this->translate('Service Groups'), N_('Service Groups'),
'/monitoring/list/servicegroups' 'monitoring/list/servicegroups'
); );
$dashboard->add( $dashboard->add(
$this->translate('Host Groups'), N_('Host Groups'),
'/monitoring/list/hostgroups' 'monitoring/list/hostgroups'
); );
/* /*
* Most Overdue * Most Overdue
*/ */
$dashboard = $this->dashboard($this->translate('Overdue')); $dashboard = $this->dashboard(N_('Overdue'), array('priority' => 70));
$dashboard->add( $dashboard->add(
$this->translate('Late Host Check Results'), N_('Late Host Check Results'),
'monitoring/list/hosts?host_next_update<now' 'monitoring/list/hosts?host_next_update<now'
); );
$dashboard->add( $dashboard->add(
$this->translate('Late Service Check Results'), N_('Late Service Check Results'),
'monitoring/list/services?service_next_update<now' 'monitoring/list/services?service_next_update<now'
); );
$dashboard->add( $dashboard->add(
$this->translate('Acknowledgements Active For At Least Three Days'), N_('Acknowledgements Active For At Least Three Days'),
'monitoring/list/comments?comment_type=Ack&comment_timestamp<-3 days&sort=comment_timestamp&dir=asc' 'monitoring/list/comments?comment_type=Ack&comment_timestamp<-3 days&sort=comment_timestamp&dir=asc'
); );
$dashboard->add( $dashboard->add(
$this->translate('Downtimes Active For More Than Three Days'), N_('Downtimes Active For More Than Three Days'),
'monitoring/list/downtimes?downtime_is_in_effect=1&downtime_scheduled_start<-3%20days&sort=downtime_start&dir=asc' 'monitoring/list/downtimes?downtime_is_in_effect=1&downtime_scheduled_start<-3%20days&sort=downtime_start&dir=asc'
); );
/* /*
* Muted Objects * Muted Objects
*/ */
$dashboard = $this->dashboard($this->translate('Muted')); $dashboard = $this->dashboard(N_('Muted'), array('priority' => 80));
$dashboard->add( $dashboard->add(
$this->translate('Disabled Service Notifications'), N_('Disabled Service Notifications'),
'monitoring/list/services?service_notifications_enabled=0&limit=10' 'monitoring/list/services?service_notifications_enabled=0&limit=10'
); );
$dashboard->add( $dashboard->add(
$this->translate('Disabled Host Notifications'), N_('Disabled Host Notifications'),
'monitoring/list/hosts?host_notifications_enabled=0&limit=10' 'monitoring/list/hosts?host_notifications_enabled=0&limit=10'
); );
$dashboard->add( $dashboard->add(
$this->translate('Disabled Service Checks'), N_('Disabled Service Checks'),
'monitoring/list/services?service_active_checks_enabled=0&limit=10' 'monitoring/list/services?service_active_checks_enabled=0&limit=10'
); );
$dashboard->add( $dashboard->add(
$this->translate('Disabled Host Checks'), N_('Disabled Host Checks'),
'monitoring/list/hosts?host_active_checks_enabled=0&limit=10' 'monitoring/list/hosts?host_active_checks_enabled=0&limit=10'
); );
$dashboard->add( $dashboard->add(
$this->translate('Acknowledged Problem Services'), N_('Acknowledged Problem Services'),
'monitoring/list/services?service_acknowledgement_type=2&service_problem=1&sort=service_state&limit=10' 'monitoring/list/services?service_acknowledgement_type=2&service_problem=1&sort=service_state&limit=10'
); );
$dashboard->add( $dashboard->add(
$this->translate('Acknowledged Problem Hosts'), N_('Acknowledged Problem Hosts'),
'monitoring/list/hosts?host_acknowledgement_type=2&host_problem=1&sort=host_severity&limit=10' 'monitoring/list/hosts?host_acknowledgement_type=2&host_problem=1&sort=host_severity&limit=10'
); );
/* /*
* Activity Stream * Activity Stream
*/ */
$dashboard = $this->dashboard($this->translate('Activity Stream')); $dashboard = $this->dashboard(N_('Activity Stream'), array('priority' => 90));
$dashboard->add( $dashboard->add(
$this->translate('Recent Events'), N_('Recent Events'),
'monitoring/list/eventhistory?timestamp>=-3%20days&sort=timestamp&dir=desc&limit=8' 'monitoring/list/eventhistory?timestamp>=-3%20days&sort=timestamp&dir=desc&limit=8'
); );
$dashboard->add( $dashboard->add(
$this->translate('Recent Hard State Changes'), N_('Recent Hard State Changes'),
'monitoring/list/eventhistory?timestamp>=-3%20days&type=hard_state&sort=timestamp&dir=desc&limit=8' 'monitoring/list/eventhistory?timestamp>=-3%20days&type=hard_state&sort=timestamp&dir=desc&limit=8'
); );
$dashboard->add( $dashboard->add(
$this->translate('Recent Notifications'), N_('Recent Notifications'),
'monitoring/list/eventhistory?timestamp>=-3%20days&type=notify&sort=timestamp&dir=desc&limit=8' 'monitoring/list/eventhistory?timestamp>=-3%20days&type=notify&sort=timestamp&dir=desc&limit=8'
); );
$dashboard->add( $dashboard->add(
$this->translate('Downtimes Recently Started'), N_('Downtimes Recently Started'),
'monitoring/list/eventhistory?timestamp>=-3%20days&type=dt_start&sort=timestamp&dir=desc&limit=8' 'monitoring/list/eventhistory?timestamp>=-3%20days&type=dt_start&sort=timestamp&dir=desc&limit=8'
); );
$dashboard->add( $dashboard->add(
$this->translate('Downtimes Recently Ended'), N_('Downtimes Recently Ended'),
'monitoring/list/eventhistory?timestamp>=-3%20days&type=dt_end&sort=timestamp&dir=desc&limit=8' 'monitoring/list/eventhistory?timestamp>=-3%20days&type=dt_end&sort=timestamp&dir=desc&limit=8'
); );
/* /*
* Stats * Stats
*/ */
$dashboard = $this->dashboard($this->translate('Stats')); $dashboard = $this->dashboard(N_('Stats'), array('priority' => 99));
$dashboard->add( $dashboard->add(
$this->translate('Check Stats'), N_('Check Stats'),
'monitoring/health/stats' 'monitoring/health/stats'
); );
$dashboard->add( $dashboard->add(
$this->translate('Process Information'), N_('Process Information'),
'monitoring/health/info' 'monitoring/health/info'
); );

View File

@ -5,9 +5,11 @@ namespace Icinga\Module\Monitoring\Object;
use InvalidArgumentException; use InvalidArgumentException;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Data\Filter\Filter; use Icinga\Data\Filter\Filter;
use Icinga\Data\Filterable; use Icinga\Data\Filterable;
use Icinga\Exception\InvalidPropertyException; use Icinga\Exception\InvalidPropertyException;
use Icinga\Exception\ProgrammingError;
use Icinga\Module\Monitoring\Backend\MonitoringBackend; use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\UrlParams; use Icinga\Web\UrlParams;
@ -208,6 +210,59 @@ abstract class MonitoredObject implements Filterable
// Left out on purpose. Interface is deprecated. // Left out on purpose. Interface is deprecated.
} }
/**
* Return whether this object matches the given filter
*
* @param Filter $filter
*
* @return bool
*
* @throws ProgrammingError In case the object cannot be found
*/
public function matches(Filter $filter)
{
if ($this->properties === null && $this->fetch() === false) {
throw new ProgrammingError(
'Unable to apply filter. Object %s of type %s not found.',
$this->getName(),
$this->getType()
);
}
$queryString = $filter->toQueryString();
$row = clone $this->properties;
if (strpos($queryString, '_host_') !== false || strpos($queryString, '_service_') !== false) {
if ($this->customvars === null) {
$this->fetchCustomvars();
}
foreach ($this->customvars as $name => $value) {
if (! is_object($value)) {
$row->{'_' . $this->getType() . '_' . strtolower(str_replace(' ', '_', $name))} = $value;
}
}
}
if (strpos($queryString, 'hostgroup_name') !== false) {
if ($this->hostgroups === null) {
$this->fetchHostgroups();
}
$row->hostgroup_name = array_keys($this->hostgroups);
}
if (strpos($queryString, 'servicegroup_name') !== false) {
if ($this->servicegroups === null) {
$this->fetchServicegroups();
}
$row->servicegroup_name = array_keys($this->servicegroups);
}
return $filter->matches($row);
}
/** /**
* Require the object's type to be one of the given types * Require the object's type to be one of the given types
* *
@ -529,12 +584,15 @@ abstract class MonitoredObject implements Filterable
*/ */
public function fetchServicegroups() public function fetchServicegroups()
{ {
$this->servicegroups = $this->backend->select() $query = $this->backend->select()
->from('servicegroup', array('servicegroup_name', 'servicegroup_alias')) ->from('servicegroup', array('servicegroup_name', 'servicegroup_alias'))
->where('host_name', $this->host_name) ->where('host_name', $this->host_name);
->where('service_description', $this->service_description)
->applyFilter($this->getFilter()) if ($this->type === self::TYPE_SERVICE) {
->fetchPairs(); $query->where('service_description', $this->service_description);
}
$this->servicegroups = $query->applyFilter($this->getFilter())->fetchPairs();
return $this; return $this;
} }

View File

@ -0,0 +1,109 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Navigation;
use Icinga\Data\Filter\Filter;
use Icinga\Web\Navigation\NavigationItem;
use Icinga\Module\Monitoring\Object\Macro;
use Icinga\Module\Monitoring\Object\MonitoredObject;
/**
* Action for monitored objects
*/
class Action extends NavigationItem
{
/**
* Whether this action's macros were already resolved
*
* @var bool
*/
protected $resolved = false;
/**
* This action's object
*
* @var MonitoredObject
*/
protected $object;
/**
* The filter to use when being asked whether to render this action
*
* @var string
*/
protected $filter;
/**
* Set this action's object
*
* @param MonitoredObject $object
*
* @return $this
*/
public function setObject(MonitoredObject $object)
{
$this->object = $object;
return $this;
}
/**
* Return this action's object
*
* @return MonitoredObject
*/
public function getObject()
{
return $this->object;
}
/**
* Set the filter to use when being asked whether to render this action
*
* @param string $filter
*
* @return $this
*/
public function setFilter($filter)
{
$this->filter = $filter;
return $this;
}
/**
* Return the filter to use when being asked whether to render this action
*
* @return string
*/
public function getFilter()
{
return $this->filter;
}
/**
* {@inheritdoc}
*/
public function getUrl()
{
$url = parent::getUrl();
if (! $this->resolved && $url !== null) {
$this->setUrl(Macro::resolveMacros($url->getAbsoluteUrl(), $this->getObject()));
$this->resolved = true;
}
return $url;
}
/**
* {@inheritdoc}
*/
public function getRender()
{
if ($this->render === null) {
$filter = $this->getFilter();
$this->render = $filter ? $this->getObject()->matches(Filter::fromQueryString($filter)) : true;
}
return $this->render;
}
}

View File

@ -0,0 +1,11 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Navigation;
/**
* A host action
*/
class HostAction extends Action
{
}

View File

@ -0,0 +1,11 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Navigation;
/**
* A host note
*/
class HostNote extends Action
{
}

View File

@ -1,13 +1,13 @@
<?php <?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Menu; namespace Icinga\Module\Monitoring\Web\Navigation\Renderer;
use Icinga\Web\Menu; use Exception;
use Icinga\Web\Menu\BadgeMenuItemRenderer; use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
use Icinga\Module\Monitoring\Backend\MonitoringBackend; use Icinga\Module\Monitoring\Backend\MonitoringBackend;
class BackendAvailabilityMenuItemRenderer extends BadgeMenuItemRenderer class BackendAvailabilityNavigationItemRenderer extends BadgeNavigationItemRenderer
{ {
/** /**
* Get whether or not the monitoring backend is currently running * Get whether or not the monitoring backend is currently running
@ -43,10 +43,15 @@ class BackendAvailabilityMenuItemRenderer extends BadgeMenuItemRenderer
*/ */
public function getCount() public function getCount()
{ {
if (! $this->isCurrentlyRunning()) { try {
return 1; if ($this->isCurrentlyRunning()) {
return 0;
}
} catch (Exception $_) {
// pass
} }
return 0;
return 1;
} }
/** /**

View File

@ -1,23 +1,26 @@
<?php <?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Menu; namespace Icinga\Module\Monitoring\Web\Navigation\Renderer;
use Exception;
use Icinga\Authentication\Auth; use Icinga\Authentication\Auth;
use Icinga\Data\ConfigObject;
use Icinga\Data\Filter\Filter; use Icinga\Data\Filter\Filter;
use Icinga\Data\Filterable; use Icinga\Data\Filterable;
use Icinga\Web\Menu;
use Icinga\Module\Monitoring\Backend\MonitoringBackend; use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Menu\BadgeMenuItemRenderer; use Icinga\Web\Navigation\Renderer\SummaryNavigationItemRenderer;
/** /**
* Render generic dataView columns as badges in MenuItems * Render generic dataView columns as badges in MenuItems
* *
* Renders numeric data view column values into menu item badges, fully configurable * Renders numeric data view column values into menu item badges, fully configurable
* and with a caching mechanism to prevent needless requests to the same data view * and with a caching mechanism to prevent needless requests to the same data view.
*
* It is possible to configure the class of the rendered badge as option 'class', the
* column to fetch using the option 'column' and the dataView from which the columns
* will be fetched using the option 'dataView'.
*/ */
class MonitoringBadgeMenuItemRenderer extends BadgeMenuItemRenderer class MonitoringBadgeNavigationItemRenderer extends SummaryNavigationItemRenderer
{ {
/** /**
* Caches the responses for all executed summaries * Caches the responses for all executed summaries
@ -35,7 +38,7 @@ class MonitoringBadgeMenuItemRenderer extends BadgeMenuItemRenderer
protected static $dataViews = array(); protected static $dataViews = array();
/** /**
* The data view displayed by this menu item * The dataview referred to by the navigation item
* *
* @var string * @var string
*/ */
@ -49,36 +52,56 @@ class MonitoringBadgeMenuItemRenderer extends BadgeMenuItemRenderer
protected $columns; protected $columns;
/** /**
* The titles that will be used to render this menu item tooltip * Set the dataview referred to by the navigation item
* *
* @var String[] * @param string $dataView
*
* @return $this
*/ */
protected $titles; public function setDataView($dataView)
/**
* The class of the badge element
*
* @var string
*/
protected $state;
/**
* Create a new instance of ColumnMenuItemRenderer
*
* It is possible to configure the class of the rendered badge as option 'class', the column
* to fetch using the option 'column' and the dataView from which the columns will be
* fetched using the option 'dataView'.
*
* @param $configuration ConfigObject The configuration to use
*/
public function __construct(ConfigObject $configuration)
{ {
parent::__construct($configuration); $this->dataView = $dataView;
return $this;
}
$this->columns = $configuration->get('columns'); /**
$this->state = $configuration->get('state'); * Return the dataview referred to by the navigation item
$this->dataView = $configuration->get('dataView'); *
* @return string
*/
public function getDataView()
{
return $this->dataView;
}
/**
* Set the columns and titles displayed in the badge
*
* @param array $columns
*
* @return $this
*/
public function setColumns(array $columns)
{
$this->columns = $columns;
return $this;
}
/**
* Return the columns and titles displayed in the badge
*
* @return array
*/
public function getColumns()
{
return $this->columns;
}
/**
* {@inheritdoc}
*/
public function init()
{
// clear the outdated summary cache, since new columns are being added. Optimally all menu item are constructed // clear the outdated summary cache, since new columns are being added. Optimally all menu item are constructed
// before any rendering is going on to avoid trashing too man old requests // before any rendering is going on to avoid trashing too man old requests
if (isset(self::$summaries[$this->dataView])) { if (isset(self::$summaries[$this->dataView])) {
@ -89,11 +112,11 @@ class MonitoringBadgeMenuItemRenderer extends BadgeMenuItemRenderer
if (! isset(self::$dataViews[$this->dataView])) { if (! isset(self::$dataViews[$this->dataView])) {
self::$dataViews[$this->dataView] = array(); self::$dataViews[$this->dataView] = array();
} }
foreach ($this->columns as $column => $title) { foreach ($this->columns as $column => $title) {
if (! array_search($column, self::$dataViews[$this->dataView])) { if (! array_search($column, self::$dataViews[$this->dataView])) {
self::$dataViews[$this->dataView][] = $column; self::$dataViews[$this->dataView][] = $column;
} }
$this->titles[$column] = $title;
} }
} }
@ -116,12 +139,11 @@ class MonitoringBadgeMenuItemRenderer extends BadgeMenuItemRenderer
} }
/** /**
* Fetch the response from the database or access cache * Fetch the dataview from the database or access cache
* *
* @param $view * @param string $view
* *
* @return null * @return object
* @throws \Icinga\Exception\ConfigurationError
*/ */
protected static function summary($view) protected static function summary($view)
{ {
@ -133,51 +155,29 @@ class MonitoringBadgeMenuItemRenderer extends BadgeMenuItemRenderer
static::applyRestriction('monitoring/filter/objects', $summary); static::applyRestriction('monitoring/filter/objects', $summary);
self::$summaries[$view] = $summary->fetchRow(); self::$summaries[$view] = $summary->fetchRow();
} }
return isset(self::$summaries[$view]) ? self::$summaries[$view] : null;
return self::$summaries[$view];
} }
/** /**
* Defines the color of the badge * {@inheritdoc}
*
* @return string
*/
public function getState()
{
return $this->state;
}
/**
* The amount of items to display in the badge
*
* @return int
*/ */
public function getCount() public function getCount()
{ {
$sum = self::summary($this->dataView); try {
$count = 0; $summary = self::summary($this->getDataView());
} catch (Exception $_) {
return 0;
}
foreach ($this->columns as $col => $title) { $count = 0;
if (isset($sum->$col)) { foreach ($this->getColumns() as $column => $title) {
$count += $sum->$col; if (isset($summary->$column) && $summary->$column > 0) {
$this->titles[] = sprintf($title, $summary->$column);
$count += $summary->$column;
} }
} }
return $count; return $count;
} }
/**
* The tooltip title
*
* @return string
*/
public function getTitle()
{
$titles = array();
$sum = $this->summary($this->dataView);
foreach ($this->columns as $column => $value) {
if (isset($sum->$column) && $sum->$column > 0) {
$titles[] = sprintf($this->titles[$column], $sum->$column);
}
}
return implode(', ', $titles);
}
} }

View File

@ -0,0 +1,11 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Navigation;
/**
* A service action
*/
class ServiceAction extends Action
{
}

View File

@ -0,0 +1,11 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Monitoring\Web\Navigation;
/**
* A service note
*/
class ServiceNote extends Action
{
}

View File

@ -290,6 +290,7 @@ class GettextTranslationHelper
'--keyword=t:1,2c', '--keyword=t:1,2c',
'--keyword=tp:1,2', '--keyword=tp:1,2',
'--keyword=tp:1,2,4c', '--keyword=tp:1,2,4c',
'--keyword=N_',
'--sort-output', '--sort-output',
'--force-po', '--force-po',
'--omit-header', '--omit-header',

View File

@ -36,6 +36,11 @@ input, select, textarea {
font-family: Calibri, Helvetica, sans-serif; font-family: Calibri, Helvetica, sans-serif;
} }
input[type=checkbox] {
margin-top: 0.3em;
margin-bottom: 0.1em;
}
input:focus, select:focus { input:focus, select:focus {
border-color: #333; border-color: #333;
} }
@ -360,6 +365,10 @@ i.autosubmit-warning {
} }
} }
input[type=checkbox] + i.autosubmit-warning {
margin-top: 0.15em;
}
html.no-js i.autosubmit-warning { html.no-js i.autosubmit-warning {
.sr-only; .sr-only;
} }

View File

@ -57,15 +57,11 @@
* TODO: How should we handle POST requests? e.g. search VS login * TODO: How should we handle POST requests? e.g. search VS login
*/ */
pushCurrentState: function () { pushCurrentState: function () {
var icinga = this.icinga;
// No history API, no action // No history API, no action
if (!this.enabled) { if (! this.enabled) {
return; return;
} }
icinga.logger.debug('Pushing current state to history');
var url = ''; var url = '';
// We only store URLs of containers sitting directly under #main: // We only store URLs of containers sitting directly under #main:
@ -85,6 +81,7 @@
// Did we find any URL? Then push it! // Did we find any URL? Then push it!
if (url !== '') { if (url !== '') {
this.icinga.logger.debug('Pushing current state to history');
this.push(url); this.push(url);
} }
}, },
@ -112,6 +109,9 @@
push: function (url) { push: function (url) {
url = url.replace(/[\?&]?_(render|reload)=[a-z0-9]+/g, ''); url = url.replace(/[\?&]?_(render|reload)=[a-z0-9]+/g, '');
if (this.lastPushUrl === url) { if (this.lastPushUrl === url) {
this.icinga.logger.debug(
'Ignoring history state push for url ' + url + ' as it\' currently on top of the stack'
);
return; return;
} }
this.lastPushUrl = url; this.lastPushUrl = url;

View File

@ -331,7 +331,7 @@
} }
} }
this.redirectToUrl(redirect, req.$target, req.getResponseHeader('X-Icinga-Rerender-Layout')); this.redirectToUrl(redirect, req.$target, req.url, req.getResponseHeader('X-Icinga-Rerender-Layout'));
return true; return true;
}, },
@ -340,9 +340,10 @@
* *
* @param {string} url * @param {string} url
* @param {object} $target * @param {object} $target
* @param {string] origin
* @param {boolean} rerenderLayout * @param {boolean} rerenderLayout
*/ */
redirectToUrl: function (url, $target, rerenderLayout) { redirectToUrl: function (url, $target, origin, rerenderLayout) {
var icinga = this.icinga; var icinga = this.icinga;
if (typeof rerenderLayout === 'undefined') { if (typeof rerenderLayout === 'undefined') {
@ -365,7 +366,15 @@
// Retain detail URL if the layout is rerendered // Retain detail URL if the layout is rerendered
parts = document.location.hash.split('#!').splice(1); parts = document.location.hash.split('#!').splice(1);
if (parts.length) { if (parts.length) {
r.loadNext = parts; r.loadNext = $.grep(parts, function (url) {
if (url !== origin) {
icinga.logger.debug('Retaining detail url ' + url);
return true;
}
icinga.logger.debug('Discarding detail url ' + url + ' as it\'s the origin of the redirect');
return false;
});
} }
} }
} else { } else {
@ -609,7 +618,7 @@
this.processRedirectHeader(req); this.processRedirectHeader(req);
if (typeof req.loadNext !== 'undefined') { if (typeof req.loadNext !== 'undefined' && req.loadNext.length) {
if ($('#col2').length) { if ($('#col2').length) {
this.loadUrl(req.loadNext[0], $('#col2')); this.loadUrl(req.loadNext[0], $('#col2'));
this.icinga.ui.layout2col(); this.icinga.ui.layout2col();

View File

@ -82,13 +82,13 @@ class UrlTest extends BaseTestCase
$url = Url::fromPath('/my/test/url.html'); $url = Url::fromPath('/my/test/url.html');
$this->assertEquals( $this->assertEquals(
'/', '',
$url->getBaseUrl(), $url->getBaseUrl(),
'Url::fromPath does not recognize the correct base url' 'Url::fromPath does not recognize the correct base url'
); );
$this->assertEquals( $this->assertEquals(
'my/test/url.html', '/my/test/url.html',
$url->getPath(), $url->getAbsoluteUrl(),
'Url::fromPath does not recognize the correct url path' 'Url::fromPath does not recognize the correct url path'
); );
} }

View File

@ -9,7 +9,6 @@ require_once realpath(dirname(__FILE__) . '/../../../../bootstrap.php');
use Mockery; use Mockery;
use Icinga\Test\BaseTestCase; use Icinga\Test\BaseTestCase;
use Icinga\User;
use Icinga\Web\Widget\Dashboard; use Icinga\Web\Widget\Dashboard;
use Icinga\Web\Widget\Dashboard\Pane; use Icinga\Web\Widget\Dashboard\Pane;
use Icinga\Web\Widget\Dashboard\Dashlet; use Icinga\Web\Widget\Dashboard\Dashlet;
@ -46,23 +45,6 @@ class DashboardTest extends BaseTestCase
Mockery::close(); // Necessary because some tests run in a separate process Mockery::close(); // Necessary because some tests run in a separate process
} }
public function setUp()
{
$moduleMock = Mockery::mock('Icinga\Application\Modules\Module');
$moduleMock->shouldReceive('getPaneItems')->andReturn(array(
'test-pane' => new Pane('Test Pane')
));
$moduleMock->shouldReceive('getName')->andReturn('test');
$moduleManagerMock = Mockery::mock('Icinga\Application\Modules\Manager');
$moduleManagerMock->shouldReceive('getLoadedModules')->andReturn(array(
'test-module' => $moduleMock
));
$bootstrapMock = $this->setupIcingaMock();
$bootstrapMock->shouldReceive('getModuleManager')->andReturn($moduleManagerMock);
}
public function testWhetherCreatePaneCreatesAPane() public function testWhetherCreatePaneCreatesAPane()
{ {
$dashboard = new Dashboard(); $dashboard = new Dashboard();
@ -126,24 +108,6 @@ class DashboardTest extends BaseTestCase
); );
} }
/**
* @depends testWhetherCreatePaneCreatesAPane
*/
public function testLoadPaneItemsProvidedByEnabledModules()
{
$user = new User('test');
$user->setPermissions(array('*' => '*'));
$dashboard = new Dashboard();
$dashboard->setUser($user);
$dashboard->load();
$this->assertCount(
1,
$dashboard->getPanes(),
'Dashboard::load() could not load panes from enabled modules'
);
}
/** /**
* @expectedException \Icinga\Exception\ProgrammingError * @expectedException \Icinga\Exception\ProgrammingError
* @depends testWhetherCreatePaneCreatesAPane * @depends testWhetherCreatePaneCreatesAPane