diff --git a/application/controllers/NavigationController.php b/application/controllers/NavigationController.php index f5c1d785a..139c22741 100644 --- a/application/controllers/NavigationController.php +++ b/application/controllers/NavigationController.php @@ -5,13 +5,13 @@ namespace Icinga\Controllers; use Exception; use Icinga\Application\Config; -use Icinga\Application\Icinga; use Icinga\Exception\NotFoundError; use Icinga\Data\DataArray\ArrayDatasource; use Icinga\Forms\ConfirmRemovalForm; use Icinga\Forms\Navigation\NavigationConfigForm; use Icinga\Web\Controller; use Icinga\Web\Form; +use Icinga\Web\Navigation\Navigation; use Icinga\Web\Notification; use Icinga\Web\Url; @@ -21,11 +21,11 @@ use Icinga\Web\Url; class NavigationController extends Controller { /** - * The default item types provided by Icinga Web 2 + * The global navigation item type configuration * * @var array */ - protected $defaultItemTypes; + protected $itemTypeConfig; /** * {@inheritdoc} @@ -33,11 +33,19 @@ class NavigationController extends Controller public function init() { parent::init(); + $this->itemTypeConfig = Navigation::getItemTypeConfiguration(); + } - $this->defaultItemTypes = array( - 'menu-item' => $this->translate('Menu Entry'), - 'dashlet' => 'Dashlet' - ); + /** + * Return the label for the given navigation item type + * + * @param string $type + * + * @return string $type if no label can be found + */ + protected function getItemLabel($type) + { + return isset($this->itemTypeConfig[$type]['label']) ? $this->itemTypeConfig[$type]['label'] : $type; } /** @@ -47,33 +55,71 @@ class NavigationController extends Controller */ protected function listItemTypes() { - $moduleManager = Icinga::app()->getModuleManager(); - - $types = $this->defaultItemTypes; - foreach ($moduleManager->getLoadedModules() as $module) { - if ($this->hasPermission($moduleManager::MODULE_PERMISSION_NS . $module->getName())) { - $moduleTypes = $module->getNavigationItems(); - if (! empty($moduleTypes)) { - $types = array_merge($types, $moduleTypes); - } - } + $types = array(); + foreach ($this->itemTypeConfig as $type => $options) { + $types[$type] = isset($options['label']) ? $options['label'] : $type; } return $types; } + /** + * Return all shared navigation item configurations + * + * @param string $owner A username if only items shared by a specific user are desired + * + * @return array + */ + protected function fetchSharedNavigationItemConfigs($owner = null) + { + $configs = array(); + foreach ($this->itemTypeConfig as $type => $_) { + $config = Config::navigation($type); + $config->getConfigObject()->setKeyColumn('name'); + $query = $config->select(); + if ($owner !== null) { + $query->where('owner', $owner); + } + + foreach ($query as $itemConfig) { + $configs[] = $itemConfig; + } + } + + return $configs; + } + + /** + * Return all user navigation item configurations + * + * @param string $username + * + * @return array + */ + protected function fetchUserNavigationItemConfigs($username) + { + $configs = array(); + foreach ($this->itemTypeConfig as $type => $_) { + $config = Config::navigation($type, $username); + $config->getConfigObject()->setKeyColumn('name'); + foreach ($config->select() as $itemConfig) { + $configs[] = $itemConfig; + } + } + + return $configs; + } + /** * Show the current user a list of his/her navigation items */ public function indexAction() { $user = $this->Auth()->getUser(); - $ds = new ArrayDatasource(array_merge( - Config::app('navigation')->select()->where('owner', $user->getUsername())->fetchAll(), - iterator_to_array($user->loadNavigationConfig()) + $this->fetchSharedNavigationItemConfigs($user->getUsername()), + $this->fetchUserNavigationItemConfigs($user->getUsername()) )); - $ds->setKeyColumn('name'); $query = $ds->select(); $this->view->types = $this->listItemTypes(); @@ -91,7 +137,7 @@ class NavigationController extends Controller array( 'type' => $this->translate('Type'), 'owner' => $this->translate('Shared'), - 'name' => $this->translate('Shared Navigation') + 'name' => $this->translate('Navigation') ), $query ); @@ -103,13 +149,11 @@ class NavigationController extends Controller public function sharedAction() { $this->assertPermission('config/application/navigation'); - $config = Config::app('navigation'); - $config->getConfigObject()->setKeyColumn('name'); - $query = $config->select(); + $ds = new ArrayDatasource($this->fetchSharedNavigationItemConfigs()); + $query = $ds->select(); $removeForm = new Form(); $removeForm->setUidDisabled(); - $removeForm->setAction(Url::fromPath('navigation/unshare')); $removeForm->addElement('hidden', 'name', array( 'decorators' => array('ViewHelper') )); @@ -156,11 +200,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) { diff --git a/application/forms/Navigation/NavigationConfigForm.php b/application/forms/Navigation/NavigationConfigForm.php index ba1f20d30..94ff860ae 100644 --- a/application/forms/Navigation/NavigationConfigForm.php +++ b/application/forms/Navigation/NavigationConfigForm.php @@ -123,12 +123,18 @@ class NavigationConfigForm extends ConfigForm /** * Return the user's navigation configuration * + * @param string $type + * * @return Config */ - public function getUserConfig() + public function getUserConfig($type = null) { if ($this->userConfig === null) { - $this->setUserConfig($this->getUser()->loadNavigationConfig()); + if ($type === null) { + throw new ProgrammingError('You need to pass a type if no user configuration is set'); + } + + $this->setUserConfig(Config::navigation($type, $this->getUser()->getUsername())); } return $this->userConfig; @@ -151,10 +157,20 @@ class NavigationConfigForm extends ConfigForm /** * Return the shared navigation configuration * + * @param string $type + * * @return Config */ - public function getShareConfig() + public function getShareConfig($type = null) { + if ($this->shareConfig === null) { + if ($type === null) { + throw new ProgrammingError('You need to pass a type if no share configuration is set'); + } + + $this->setShareConfig(Config::navigation($type)); + } + return $this->shareConfig; } @@ -194,10 +210,9 @@ class NavigationConfigForm extends ConfigForm $children = $this->itemToLoad ? $this->getFlattenedChildren($this->itemToLoad) : array(); $names = array(); - foreach ($this->getShareConfig() as $sectionName => $sectionConfig) { + foreach ($this->getShareConfig($type) as $sectionName => $sectionConfig) { if ( $sectionName !== $this->itemToLoad - && $sectionConfig->type === $type && $sectionConfig->owner === ($owner ?: $this->getUser()->getUsername()) && !in_array($sectionName, $children, true) ) { @@ -205,10 +220,9 @@ class NavigationConfigForm extends ConfigForm } } - foreach ($this->getUserConfig() as $sectionName => $sectionConfig) { + foreach ($this->getUserConfig($type) as $sectionName => $sectionConfig) { if ( $sectionName !== $this->itemToLoad - && $sectionConfig->type === $type && !in_array($sectionName, $children, true) ) { $names[] = $sectionName; @@ -271,29 +285,31 @@ class NavigationConfigForm extends ConfigForm * * @return $this * - * @throws InvalidArgumentException In case $data does not contain a navigation item name + * @throws InvalidArgumentException In case $data does not contain a navigation item name or type * @throws IcingaException In case a navigation item with the same name already exists */ public function add(array $data) { if (! isset($data['name'])) { throw new InvalidArgumentException('Key \'name\' missing'); + } elseif (! isset($data['type'])) { + throw new InvalidArgumentException('Key \'type\' missing'); } $shared = false; - $config = $this->getUserConfig(); + $config = $this->getUserConfig($data['type']); if ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) { if ($this->getUser()->can('application/share/navigation')) { $data['owner'] = $this->getUser()->getUsername(); - $config = $this->getShareConfig(); + $config = $this->getShareConfig($data['type']); $shared = true; } else { unset($data['users']); unset($data['groups']); } - } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'])) { + } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'], $data['type'])) { $data['owner'] = $this->getUser()->getUsername(); - $config = $this->getShareConfig(); + $config = $this->getShareConfig($data['type']); $shared = true; } @@ -301,9 +317,9 @@ class NavigationConfigForm extends ConfigForm $exists = $config->hasSection($itemName); if (! $exists) { if ($shared) { - $exists = $this->getUserConfig()->hasSection($itemName); + $exists = $this->getUserConfig($data['type'])->hasSection($itemName); } else { - $exists = (bool) $this->getShareConfig() + $exists = (bool) $this->getShareConfig($data['type']) ->select() ->where('name', $itemName) ->where('owner', $this->getUser()->getUsername()) @@ -385,8 +401,7 @@ class NavigationConfigForm extends ConfigForm if ($ownerName === $this->getUser()->getUsername()) { $exists = $this->getUserConfig()->hasSection($name); } else { - $owner = new User($ownerName); - $exists = $owner->loadNavigationConfig()->hasSection($name); + $exists = Config::navigation($itemConfig->type, $ownerName)->hasSection($name); } } else { $exists = (bool) $this->getShareConfig() @@ -521,8 +536,7 @@ class NavigationConfigForm extends ConfigForm if (! $itemConfig->owner || $itemConfig->owner === $this->getUser()->getUsername()) { $config = $this->getUserConfig(); } else { - $owner = new User($itemConfig->owner); - $config = $owner->loadNavigationConfig(); + $config = Config::navigation($itemConfig->type, $itemConfig->owner); } foreach ($children as $child) { @@ -549,6 +563,13 @@ class NavigationConfigForm extends ConfigForm $shared = false; $itemTypes = $this->getItemTypes(); $itemType = isset($formData['type']) ? $formData['type'] : key($itemTypes); + if ($itemType === null) { + throw new ProgrammingError( + 'This should actually not happen. Create a bug report at dev.icinga.org' + . ' or remove this assertion if you know what you\'re doing' + ); + } + $itemForm = $this->getItemForm($itemType); $this->addElement( @@ -606,17 +627,27 @@ class NavigationConfigForm extends ConfigForm } } - $this->addElement( - 'select', - 'type', - array( - 'required' => true, - 'autosubmit' => true, - 'label' => $this->translate('Type'), - 'description' => $this->translate('The type of this navigation item'), - 'multiOptions' => $itemTypes - ) - ); + if (empty($itemTypes) || count($itemTypes) === 1) { + $this->addElement( + 'hidden', + 'type', + array( + 'value' => $itemType + ) + ); + } else { + $this->addElement( + 'select', + 'type', + array( + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('Type'), + 'description' => $this->translate('The type of this navigation item'), + 'multiOptions' => $itemTypes + ) + ); + } if (! $shared && $itemForm->requiresParentSelection()) { if ($this->itemToLoad && $this->hasBeenShared($this->itemToLoad)) { @@ -767,12 +798,13 @@ class NavigationConfigForm extends ConfigForm * Return whether the given navigation item has been shared * * @param string $name + * @param string $type * * @return bool */ - protected function hasBeenShared($name) + protected function hasBeenShared($name, $type = null) { - return $this->getConfigForItem($name) === $this->getShareConfig(); + return $this->getShareConfig($type) === $this->getConfigForItem($name); } /** diff --git a/application/views/scripts/navigation/index.phtml b/application/views/scripts/navigation/index.phtml index 7c81ba5a8..2f77d0995 100644 --- a/application/views/scripts/navigation/index.phtml +++ b/application/views/scripts/navigation/index.phtml @@ -22,14 +22,17 @@ translate('Remove'); ?> - $item): ?> + qlink( - $name, + $item->name, 'navigation/edit', - array('name' => $name), array( - 'title' => sprintf($this->translate('Edit navigation item %s'), $name) + 'name' => $item->name, + 'type' => $item->type + ), + array( + 'title' => sprintf($this->translate('Edit navigation item %s'), $item->name) ) ); ?> type && isset($types[$item->type]) @@ -39,10 +42,13 @@ qlink( '', 'navigation/remove', - array('name' => $name), + array( + 'name' => $item->name, + 'type' => $item->type + ), array( 'icon' => 'trash', - 'title' => sprintf($this->translate('Remove navigation item %s'), $name) + 'title' => sprintf($this->translate('Remove navigation item %s'), $item->name) ) ); ?> diff --git a/application/views/scripts/navigation/shared.phtml b/application/views/scripts/navigation/shared.phtml index 939249f84..5d0a3107f 100644 --- a/application/views/scripts/navigation/shared.phtml +++ b/application/views/scripts/navigation/shared.phtml @@ -1,4 +1,8 @@ -compact): ?> +compact): ?>
tabs; ?> sortBox; ?> @@ -19,17 +23,19 @@ translate('Unshare'); ?> - $item): ?> + qlink( - $name, + $item->name, 'navigation/edit', array( - 'name' => $name, + 'name' => $item->name, + 'type' => $item->type, + 'owner' => $item->owner, 'referrer' => 'shared' ), array( - 'title' => sprintf($this->translate('Edit shared navigation item %s'), $name) + 'title' => sprintf($this->translate('Edit shared navigation item %s'), $item->name) ) ); ?> type && isset($types[$item->type]) @@ -48,7 +54,12 @@ ) ); ?> - setDefault('name', $name); ?> + setDefault('name', $item->name) + ->setAction(Url::fromPath( + 'navigation/unshare', + array('type' => $item->type, 'owner' => $item->owner) + )); ?> diff --git a/library/Icinga/Application/Config.php b/library/Icinga/Application/Config.php index bfe2ddd60..d25ec748c 100644 --- a/library/Icinga/Application/Config.php +++ b/library/Icinga/Application/Config.php @@ -13,7 +13,9 @@ use Icinga\Data\Selectable; use Icinga\Data\SimpleQuery; use Icinga\File\Ini\IniWriter; use Icinga\File\Ini\IniParser; +use Icinga\Exception\IcingaException; use Icinga\Exception\NotReadableError; +use Icinga\Web\Navigation\Navigation; /** * Container for INI like configuration and global registry of application and module related configuration. @@ -41,6 +43,13 @@ class Config implements Countable, Iterator, Selectable */ protected static $modules = array(); + /** + * Navigation config instances per type + * + * @var array + */ + protected static $navigation = array(); + /** * The internal ConfigObject * @@ -416,6 +425,60 @@ class Config implements Countable, Iterator, Selectable return $moduleConfigs[$configname]; } + /** + * Retrieve a navigation config + * + * @param string $type The type identifier of the navigation item for which to return its config + * @param string $username A user's name or null if the shared config is desired + * @param bool $fromDisk If true, the configuration will be read from disk + * + * @return Config The requested configuration + */ + public static function navigation($type, $username = null, $fromDisk = false) + { + if (! isset(self::$navigation[$type])) { + self::$navigation[$type] = array(); + } + + $branch = $username ?: 'shared'; + $typeConfigs = self::$navigation[$type]; + if (! isset($typeConfigs[$branch]) || $fromDisk) { + $typeConfigs[$branch] = static::fromIni(static::getNavigationConfigPath($type, $username)); + } + + return $typeConfigs[$branch]; + } + + /** + * Return the path to the configuration file for the given navigation item type and user + * + * @param string $type + * @param string $username + * + * @return string + * + * @throws IcingaException In case the given type is unknown + */ + protected static function getNavigationConfigPath($type, $username = null) + { + $itemTypeConfig = Navigation::getItemTypeConfiguration(); + if (! isset($itemTypeConfig[$type])) { + throw new IcingaException('Invalid navigation item type %s provided', $type); + } + + if (isset($itemTypeConfig[$type]['config'])) { + $filename = $itemTypeConfig[$type]['config'] . '.ini'; + } else { + $filename = $type . 's.ini'; + } + + return static::resolvePath( + ($username ? 'preferences' . DIRECTORY_SEPARATOR . $username : 'navigation') + . DIRECTORY_SEPARATOR + . $filename + ); + } + /** * Return this config rendered as a INI structured string * diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index ebbfc3d98..842a581a9 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -1014,16 +1014,21 @@ class Module } /** - * Provide a new type of configurable navigation item with a optional label + * Provide a new type of configurable navigation item with a optional label and config filename * * @param string $type * @param string $label + * @param string $config * * @return $this */ - protected function provideNavigationItem($type, $label = null) + protected function provideNavigationItem($type, $label = null, $config = null) { - $this->navigationItems[$type] = $label ?: $type; + $this->navigationItems[$type] = array( + 'label' => $label, + 'config' => $config + ); + return $this; } diff --git a/library/Icinga/Application/Web.php b/library/Icinga/Application/Web.php index 538afb029..da11b9c1e 100644 --- a/library/Icinga/Application/Web.php +++ b/library/Icinga/Application/Web.php @@ -179,12 +179,11 @@ class Web extends EmbeddedWeb */ public function getSharedNavigation($type) { - $config = Config::app('navigation')->getConfigObject(); - $config->setKeyColumn('name'); + $config = Config::navigation($type === 'dashboard-pane' ? 'dashlet' : $type); if ($type === 'dashboard-pane') { $panes = array(); - foreach ($config->select()->where('type', 'dashlet') as $dashletName => $dashletConfig) { + foreach ($config as $dashletName => $dashletConfig) { if ($this->hasAccessToSharedNavigationItem($dashletConfig)) { // TODO: Throw ConfigurationError if pane or url is missing $panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url; @@ -203,7 +202,7 @@ class Web extends EmbeddedWeb } } else { $items = array(); - foreach ($config->select()->where('type', $type) as $name => $typeConfig) { + foreach ($config as $name => $typeConfig) { if ($this->hasAccessToSharedNavigationItem($typeConfig)) { $items[$name] = $typeConfig; } diff --git a/library/Icinga/User.php b/library/Icinga/User.php index 114e59755..626905e9c 100644 --- a/library/Icinga/User.php +++ b/library/Icinga/User.php @@ -479,22 +479,6 @@ class User return false; } - /** - * Load and return this user's navigation configuration - * - * @return Config - */ - public function loadNavigationConfig() - { - return Config::fromIni( - Config::resolvePath('preferences') - . DIRECTORY_SEPARATOR - . $this->getUsername() - . DIRECTORY_SEPARATOR - . 'navigation.ini' - ); - } - /** * Load and return this user's configured navigation of the given type * @@ -504,12 +488,11 @@ class User */ public function getNavigation($type) { - $config = $this->loadNavigationConfig(); - $config->getConfigObject()->setKeyColumn('name'); + $config = Config::navigation($type === 'dashboard-pane' ? 'dashlet' : $type, $this->getUsername()); if ($type === 'dashboard-pane') { $panes = array(); - foreach ($config->select()->where('type', 'dashlet') as $dashletName => $dashletConfig) { + foreach ($config as $dashletName => $dashletConfig) { // TODO: Throw ConfigurationError if pane or url is missing $panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url; } @@ -525,7 +508,7 @@ class User ); } } else { - $navigation = Navigation::fromConfig($config->select()->where('type', $type)); + $navigation = Navigation::fromConfig($config); } return $navigation; diff --git a/library/Icinga/Web/Navigation/Navigation.php b/library/Icinga/Web/Navigation/Navigation.php index 9cd3423a3..55101edce 100644 --- a/library/Icinga/Web/Navigation/Navigation.php +++ b/library/Icinga/Web/Navigation/Navigation.php @@ -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 *