Merge branch 'bugfix/separate-configuration-files-for-navigation-items-10246'

fixes #10246
This commit is contained in:
Johannes Meyer 2015-09-30 12:38:45 +02:00
commit ff25c24825
9 changed files with 299 additions and 107 deletions

View File

@ -5,13 +5,13 @@ namespace Icinga\Controllers;
use Exception;
use Icinga\Application\Config;
use Icinga\Application\Icinga;
use Icinga\Exception\NotFoundError;
use Icinga\Data\DataArray\ArrayDatasource;
use Icinga\Forms\ConfirmRemovalForm;
use Icinga\Forms\Navigation\NavigationConfigForm;
use Icinga\Web\Controller;
use Icinga\Web\Form;
use Icinga\Web\Navigation\Navigation;
use Icinga\Web\Notification;
use Icinga\Web\Url;
@ -21,11 +21,11 @@ use Icinga\Web\Url;
class NavigationController extends Controller
{
/**
* The default item types provided by Icinga Web 2
* The global navigation item type configuration
*
* @var array
*/
protected $defaultItemTypes;
protected $itemTypeConfig;
/**
* {@inheritdoc}
@ -33,11 +33,19 @@ class NavigationController extends Controller
public function init()
{
parent::init();
$this->itemTypeConfig = Navigation::getItemTypeConfiguration();
}
$this->defaultItemTypes = array(
'menu-item' => $this->translate('Menu Entry'),
'dashlet' => 'Dashlet'
);
/**
* Return the label for the given navigation item type
*
* @param string $type
*
* @return string $type if no label can be found
*/
protected function getItemLabel($type)
{
return isset($this->itemTypeConfig[$type]['label']) ? $this->itemTypeConfig[$type]['label'] : $type;
}
/**
@ -47,33 +55,71 @@ class NavigationController extends Controller
*/
protected function listItemTypes()
{
$moduleManager = Icinga::app()->getModuleManager();
$types = $this->defaultItemTypes;
foreach ($moduleManager->getLoadedModules() as $module) {
if ($this->hasPermission($moduleManager::MODULE_PERMISSION_NS . $module->getName())) {
$moduleTypes = $module->getNavigationItems();
if (! empty($moduleTypes)) {
$types = array_merge($types, $moduleTypes);
}
}
$types = array();
foreach ($this->itemTypeConfig as $type => $options) {
$types[$type] = isset($options['label']) ? $options['label'] : $type;
}
return $types;
}
/**
* Return all shared navigation item configurations
*
* @param string $owner A username if only items shared by a specific user are desired
*
* @return array
*/
protected function fetchSharedNavigationItemConfigs($owner = null)
{
$configs = array();
foreach ($this->itemTypeConfig as $type => $_) {
$config = Config::navigation($type);
$config->getConfigObject()->setKeyColumn('name');
$query = $config->select();
if ($owner !== null) {
$query->where('owner', $owner);
}
foreach ($query as $itemConfig) {
$configs[] = $itemConfig;
}
}
return $configs;
}
/**
* Return all user navigation item configurations
*
* @param string $username
*
* @return array
*/
protected function fetchUserNavigationItemConfigs($username)
{
$configs = array();
foreach ($this->itemTypeConfig as $type => $_) {
$config = Config::navigation($type, $username);
$config->getConfigObject()->setKeyColumn('name');
foreach ($config->select() as $itemConfig) {
$configs[] = $itemConfig;
}
}
return $configs;
}
/**
* Show the current user a list of his/her navigation items
*/
public function indexAction()
{
$user = $this->Auth()->getUser();
$ds = new ArrayDatasource(array_merge(
Config::app('navigation')->select()->where('owner', $user->getUsername())->fetchAll(),
iterator_to_array($user->loadNavigationConfig())
$this->fetchSharedNavigationItemConfigs($user->getUsername()),
$this->fetchUserNavigationItemConfigs($user->getUsername())
));
$ds->setKeyColumn('name');
$query = $ds->select();
$this->view->types = $this->listItemTypes();
@ -91,7 +137,7 @@ class NavigationController extends Controller
array(
'type' => $this->translate('Type'),
'owner' => $this->translate('Shared'),
'name' => $this->translate('Shared Navigation')
'name' => $this->translate('Navigation')
),
$query
);
@ -103,13 +149,11 @@ class NavigationController extends Controller
public function sharedAction()
{
$this->assertPermission('config/application/navigation');
$config = Config::app('navigation');
$config->getConfigObject()->setKeyColumn('name');
$query = $config->select();
$ds = new ArrayDatasource($this->fetchSharedNavigationItemConfigs());
$query = $ds->select();
$removeForm = new Form();
$removeForm->setUidDisabled();
$removeForm->setAction(Url::fromPath('navigation/unshare'));
$removeForm->addElement('hidden', 'name', array(
'decorators' => array('ViewHelper')
));
@ -156,11 +200,10 @@ class NavigationController extends Controller
{
$form = new NavigationConfigForm();
$form->setRedirectUrl('navigation');
$form->setUser($this->Auth()->getUser());
$form->setItemTypes($this->listItemTypes());
$form->setTitle($this->translate('Create New Navigation Item'));
$form->addDescription($this->translate('Create a new navigation item, such as a menu entry or dashlet.'));
$form->setUser($this->Auth()->getUser());
$form->setShareConfig(Config::app('navigation'));
$form->setOnSuccess(function (NavigationConfigForm $form) {
$data = array_filter($form->getValues());
@ -172,7 +215,7 @@ class NavigationController extends Controller
}
if ($form->save()) {
if (isset($data['type']) && $data['type'] === 'menu-item') {
if ($data['type'] === 'menu-item') {
$form->getResponse()->setRerenderLayout();
}
@ -194,14 +237,22 @@ class NavigationController extends Controller
public function editAction()
{
$itemName = $this->params->getRequired('name');
$itemType = $this->params->getRequired('type');
$referrer = $this->params->get('referrer', 'index');
$user = $this->Auth()->getUser();
if ($user->can('config/application/navigation')) {
$itemOwner = $this->params->get('owner', $user->getUsername());
} else {
$itemOwner = $user->getUsername();
}
$form = new NavigationConfigForm();
$form->setUser($user);
$form->setShareConfig(Config::navigation($itemType));
$form->setUserConfig(Config::navigation($itemType, $itemOwner));
$form->setRedirectUrl($referrer === 'shared' ? 'navigation/shared' : 'navigation');
$form->setItemTypes($this->listItemTypes());
$form->setTitle(sprintf($this->translate('Edit Navigation Item %s'), $itemName));
$form->setUser($this->Auth()->getUser());
$form->setShareConfig(Config::app('navigation'));
$form->setTitle(sprintf($this->translate('Edit %s %s'), $this->getItemLabel($itemType), $itemName));
$form->setOnSuccess(function (NavigationConfigForm $form) use ($itemName) {
$data = array_map(
function ($v) {
@ -248,13 +299,17 @@ class NavigationController extends Controller
public function removeAction()
{
$itemName = $this->params->getRequired('name');
$itemType = $this->params->getRequired('type');
$user = $this->Auth()->getUser();
$navigationConfigForm = new NavigationConfigForm();
$navigationConfigForm->setUser($this->Auth()->getUser());
$navigationConfigForm->setShareConfig(Config::app('navigation'));
$navigationConfigForm->setUser($user);
$navigationConfigForm->setShareConfig(Config::navigation($itemType));
$navigationConfigForm->setUserConfig(Config::navigation($itemType, $user->getUsername()));
$form = new ConfirmRemovalForm();
$form->setRedirectUrl('navigation');
$form->setTitle(sprintf($this->translate('Remove Navigation Item %s'), $itemName));
$form->setTitle(sprintf($this->translate('Remove %s %s'), $this->getItemLabel($itemType), $itemName));
$form->setOnSuccess(function (ConfirmRemovalForm $form) use ($itemName, $navigationConfigForm) {
try {
$itemConfig = $navigationConfigForm->delete($itemName);
@ -291,9 +346,14 @@ class NavigationController extends Controller
$this->assertPermission('config/application/navigation');
$this->assertHttpMethod('POST');
// TODO: I'd like these being form fields
$itemType = $this->params->getRequired('type');
$itemOwner = $this->params->getRequired('owner');
$navigationConfigForm = new NavigationConfigForm();
$navigationConfigForm->setUser($this->Auth()->getUser());
$navigationConfigForm->setShareConfig(Config::app('navigation'));
$navigationConfigForm->setShareConfig(Config::navigation($itemType));
$navigationConfigForm->setUserConfig(Config::navigation($itemType, $itemOwner));
$form = new Form(array(
'onSuccess' => function ($form) use ($navigationConfigForm) {

View File

@ -123,12 +123,18 @@ class NavigationConfigForm extends ConfigForm
/**
* Return the user's navigation configuration
*
* @param string $type
*
* @return Config
*/
public function getUserConfig()
public function getUserConfig($type = null)
{
if ($this->userConfig === null) {
$this->setUserConfig($this->getUser()->loadNavigationConfig());
if ($type === null) {
throw new ProgrammingError('You need to pass a type if no user configuration is set');
}
$this->setUserConfig(Config::navigation($type, $this->getUser()->getUsername()));
}
return $this->userConfig;
@ -151,10 +157,20 @@ class NavigationConfigForm extends ConfigForm
/**
* Return the shared navigation configuration
*
* @param string $type
*
* @return Config
*/
public function getShareConfig()
public function getShareConfig($type = null)
{
if ($this->shareConfig === null) {
if ($type === null) {
throw new ProgrammingError('You need to pass a type if no share configuration is set');
}
$this->setShareConfig(Config::navigation($type));
}
return $this->shareConfig;
}
@ -194,10 +210,9 @@ class NavigationConfigForm extends ConfigForm
$children = $this->itemToLoad ? $this->getFlattenedChildren($this->itemToLoad) : array();
$names = array();
foreach ($this->getShareConfig() as $sectionName => $sectionConfig) {
foreach ($this->getShareConfig($type) as $sectionName => $sectionConfig) {
if (
$sectionName !== $this->itemToLoad
&& $sectionConfig->type === $type
&& $sectionConfig->owner === ($owner ?: $this->getUser()->getUsername())
&& !in_array($sectionName, $children, true)
) {
@ -205,10 +220,9 @@ class NavigationConfigForm extends ConfigForm
}
}
foreach ($this->getUserConfig() as $sectionName => $sectionConfig) {
foreach ($this->getUserConfig($type) as $sectionName => $sectionConfig) {
if (
$sectionName !== $this->itemToLoad
&& $sectionConfig->type === $type
&& !in_array($sectionName, $children, true)
) {
$names[] = $sectionName;
@ -271,29 +285,31 @@ class NavigationConfigForm extends ConfigForm
*
* @return $this
*
* @throws InvalidArgumentException In case $data does not contain a navigation item name
* @throws InvalidArgumentException In case $data does not contain a navigation item name or type
* @throws IcingaException In case a navigation item with the same name already exists
*/
public function add(array $data)
{
if (! isset($data['name'])) {
throw new InvalidArgumentException('Key \'name\' missing');
} elseif (! isset($data['type'])) {
throw new InvalidArgumentException('Key \'type\' missing');
}
$shared = false;
$config = $this->getUserConfig();
$config = $this->getUserConfig($data['type']);
if ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) {
if ($this->getUser()->can('application/share/navigation')) {
$data['owner'] = $this->getUser()->getUsername();
$config = $this->getShareConfig();
$config = $this->getShareConfig($data['type']);
$shared = true;
} else {
unset($data['users']);
unset($data['groups']);
}
} elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'])) {
} elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'], $data['type'])) {
$data['owner'] = $this->getUser()->getUsername();
$config = $this->getShareConfig();
$config = $this->getShareConfig($data['type']);
$shared = true;
}
@ -301,9 +317,9 @@ class NavigationConfigForm extends ConfigForm
$exists = $config->hasSection($itemName);
if (! $exists) {
if ($shared) {
$exists = $this->getUserConfig()->hasSection($itemName);
$exists = $this->getUserConfig($data['type'])->hasSection($itemName);
} else {
$exists = (bool) $this->getShareConfig()
$exists = (bool) $this->getShareConfig($data['type'])
->select()
->where('name', $itemName)
->where('owner', $this->getUser()->getUsername())
@ -385,8 +401,7 @@ class NavigationConfigForm extends ConfigForm
if ($ownerName === $this->getUser()->getUsername()) {
$exists = $this->getUserConfig()->hasSection($name);
} else {
$owner = new User($ownerName);
$exists = $owner->loadNavigationConfig()->hasSection($name);
$exists = Config::navigation($itemConfig->type, $ownerName)->hasSection($name);
}
} else {
$exists = (bool) $this->getShareConfig()
@ -521,8 +536,7 @@ class NavigationConfigForm extends ConfigForm
if (! $itemConfig->owner || $itemConfig->owner === $this->getUser()->getUsername()) {
$config = $this->getUserConfig();
} else {
$owner = new User($itemConfig->owner);
$config = $owner->loadNavigationConfig();
$config = Config::navigation($itemConfig->type, $itemConfig->owner);
}
foreach ($children as $child) {
@ -549,6 +563,13 @@ class NavigationConfigForm extends ConfigForm
$shared = false;
$itemTypes = $this->getItemTypes();
$itemType = isset($formData['type']) ? $formData['type'] : key($itemTypes);
if ($itemType === null) {
throw new ProgrammingError(
'This should actually not happen. Create a bug report at dev.icinga.org'
. ' or remove this assertion if you know what you\'re doing'
);
}
$itemForm = $this->getItemForm($itemType);
$this->addElement(
@ -606,17 +627,27 @@ class NavigationConfigForm extends ConfigForm
}
}
$this->addElement(
'select',
'type',
array(
'required' => true,
'autosubmit' => true,
'label' => $this->translate('Type'),
'description' => $this->translate('The type of this navigation item'),
'multiOptions' => $itemTypes
)
);
if (empty($itemTypes) || count($itemTypes) === 1) {
$this->addElement(
'hidden',
'type',
array(
'value' => $itemType
)
);
} else {
$this->addElement(
'select',
'type',
array(
'required' => true,
'autosubmit' => true,
'label' => $this->translate('Type'),
'description' => $this->translate('The type of this navigation item'),
'multiOptions' => $itemTypes
)
);
}
if (! $shared && $itemForm->requiresParentSelection()) {
if ($this->itemToLoad && $this->hasBeenShared($this->itemToLoad)) {
@ -767,12 +798,13 @@ class NavigationConfigForm extends ConfigForm
* Return whether the given navigation item has been shared
*
* @param string $name
* @param string $type
*
* @return bool
*/
protected function hasBeenShared($name)
protected function hasBeenShared($name, $type = null)
{
return $this->getConfigForItem($name) === $this->getShareConfig();
return $this->getShareConfig($type) === $this->getConfigForItem($name);
}
/**

View File

@ -22,14 +22,17 @@
<th style="width: 5em"><?= $this->translate('Remove'); ?></th>
</thead>
<tbody>
<?php foreach ($items as $name => $item): ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?= $this->qlink(
$name,
$item->name,
'navigation/edit',
array('name' => $name),
array(
'title' => sprintf($this->translate('Edit navigation item %s'), $name)
'name' => $item->name,
'type' => $item->type
),
array(
'title' => sprintf($this->translate('Edit navigation item %s'), $item->name)
)
); ?></td>
<td><?= $item->type && isset($types[$item->type])
@ -39,10 +42,13 @@
<td><?= $this->qlink(
'',
'navigation/remove',
array('name' => $name),
array(
'name' => $item->name,
'type' => $item->type
),
array(
'icon' => 'trash',
'title' => sprintf($this->translate('Remove navigation item %s'), $name)
'title' => sprintf($this->translate('Remove navigation item %s'), $item->name)
)
); ?></td>
</tr>

View File

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

View File

@ -13,7 +13,9 @@ use Icinga\Data\Selectable;
use Icinga\Data\SimpleQuery;
use Icinga\File\Ini\IniWriter;
use Icinga\File\Ini\IniParser;
use Icinga\Exception\IcingaException;
use Icinga\Exception\NotReadableError;
use Icinga\Web\Navigation\Navigation;
/**
* Container for INI like configuration and global registry of application and module related configuration.
@ -41,6 +43,13 @@ class Config implements Countable, Iterator, Selectable
*/
protected static $modules = array();
/**
* Navigation config instances per type
*
* @var array
*/
protected static $navigation = array();
/**
* The internal ConfigObject
*
@ -416,6 +425,60 @@ class Config implements Countable, Iterator, Selectable
return $moduleConfigs[$configname];
}
/**
* Retrieve a navigation config
*
* @param string $type The type identifier of the navigation item for which to return its config
* @param string $username A user's name or null if the shared config is desired
* @param bool $fromDisk If true, the configuration will be read from disk
*
* @return Config The requested configuration
*/
public static function navigation($type, $username = null, $fromDisk = false)
{
if (! isset(self::$navigation[$type])) {
self::$navigation[$type] = array();
}
$branch = $username ?: 'shared';
$typeConfigs = self::$navigation[$type];
if (! isset($typeConfigs[$branch]) || $fromDisk) {
$typeConfigs[$branch] = static::fromIni(static::getNavigationConfigPath($type, $username));
}
return $typeConfigs[$branch];
}
/**
* Return the path to the configuration file for the given navigation item type and user
*
* @param string $type
* @param string $username
*
* @return string
*
* @throws IcingaException In case the given type is unknown
*/
protected static function getNavigationConfigPath($type, $username = null)
{
$itemTypeConfig = Navigation::getItemTypeConfiguration();
if (! isset($itemTypeConfig[$type])) {
throw new IcingaException('Invalid navigation item type %s provided', $type);
}
if (isset($itemTypeConfig[$type]['config'])) {
$filename = $itemTypeConfig[$type]['config'] . '.ini';
} else {
$filename = $type . 's.ini';
}
return static::resolvePath(
($username ? 'preferences' . DIRECTORY_SEPARATOR . $username : 'navigation')
. DIRECTORY_SEPARATOR
. $filename
);
}
/**
* Return this config rendered as a INI structured string
*

View File

@ -1014,16 +1014,21 @@ class Module
}
/**
* Provide a new type of configurable navigation item with a optional label
* Provide a new type of configurable navigation item with a optional label and config filename
*
* @param string $type
* @param string $label
* @param string $config
*
* @return $this
*/
protected function provideNavigationItem($type, $label = null)
protected function provideNavigationItem($type, $label = null, $config = null)
{
$this->navigationItems[$type] = $label ?: $type;
$this->navigationItems[$type] = array(
'label' => $label,
'config' => $config
);
return $this;
}

View File

@ -179,12 +179,11 @@ class Web extends EmbeddedWeb
*/
public function getSharedNavigation($type)
{
$config = Config::app('navigation')->getConfigObject();
$config->setKeyColumn('name');
$config = Config::navigation($type === 'dashboard-pane' ? 'dashlet' : $type);
if ($type === 'dashboard-pane') {
$panes = array();
foreach ($config->select()->where('type', 'dashlet') as $dashletName => $dashletConfig) {
foreach ($config as $dashletName => $dashletConfig) {
if ($this->hasAccessToSharedNavigationItem($dashletConfig)) {
// TODO: Throw ConfigurationError if pane or url is missing
$panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url;
@ -203,7 +202,7 @@ class Web extends EmbeddedWeb
}
} else {
$items = array();
foreach ($config->select()->where('type', $type) as $name => $typeConfig) {
foreach ($config as $name => $typeConfig) {
if ($this->hasAccessToSharedNavigationItem($typeConfig)) {
$items[$name] = $typeConfig;
}

View File

@ -479,22 +479,6 @@ class User
return false;
}
/**
* Load and return this user's navigation configuration
*
* @return Config
*/
public function loadNavigationConfig()
{
return Config::fromIni(
Config::resolvePath('preferences')
. DIRECTORY_SEPARATOR
. $this->getUsername()
. DIRECTORY_SEPARATOR
. 'navigation.ini'
);
}
/**
* Load and return this user's configured navigation of the given type
*
@ -504,12 +488,11 @@ class User
*/
public function getNavigation($type)
{
$config = $this->loadNavigationConfig();
$config->getConfigObject()->setKeyColumn('name');
$config = Config::navigation($type === 'dashboard-pane' ? 'dashlet' : $type, $this->getUsername());
if ($type === 'dashboard-pane') {
$panes = array();
foreach ($config->select()->where('type', 'dashlet') as $dashletName => $dashletConfig) {
foreach ($config as $dashletName => $dashletConfig) {
// TODO: Throw ConfigurationError if pane or url is missing
$panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url;
}
@ -525,7 +508,7 @@ class User
);
}
} else {
$navigation = Navigation::fromConfig($config->select()->where('type', $type));
$navigation = Navigation::fromConfig($config);
}
return $navigation;

View File

@ -451,6 +451,39 @@ class Navigation implements ArrayAccess, Countable, IteratorAggregate
return $this;
}
/**
* Return the global navigation item type configuration
*
* @return array
*/
public static function getItemTypeConfiguration()
{
$defaultItemTypes = array(
'menu-item' => array(
'label' => t('Menu Entry'),
'config' => 'menu'
),
'dashlet' => array(
'label' => 'Dashlet',
'config' => 'dashboard'
)
);
$moduleItemTypes = array();
$moduleManager = Icinga::app()->getModuleManager();
foreach ($moduleManager->getLoadedModules() as $module) {
if (Auth::getInstance()->hasPermission($moduleManager::MODULE_PERMISSION_NS . $module->getName())) {
foreach ($module->getNavigationItems() as $type => $options) {
if (! isset($moduleItemTypes[$type])) {
$moduleItemTypes[$type] = $options;
}
}
}
}
return array_merge($defaultItemTypes, $moduleItemTypes);
}
/**
* Create and return a new set of navigation items for the given configuration
*