mirror of
				https://github.com/Icinga/icingaweb2.git
				synced 2025-10-31 11:24:51 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			572 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			572 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
 | |
| 
 | |
| namespace Icinga\Web\Navigation;
 | |
| 
 | |
| use ArrayAccess;
 | |
| use ArrayIterator;
 | |
| use Exception;
 | |
| use Countable;
 | |
| use InvalidArgumentException;
 | |
| 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\StringHelper;
 | |
| use Icinga\Web\Navigation\Renderer\RecursiveNavigationRenderer;
 | |
| 
 | |
| /**
 | |
|  * Container for navigation items
 | |
|  */
 | |
| 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
 | |
|      *
 | |
|      * @var int
 | |
|      */
 | |
|     const LAYOUT_DROPDOWN = 1;
 | |
| 
 | |
|     /**
 | |
|      * Flag for tabs layout
 | |
|      *
 | |
|      * @var int
 | |
|      */
 | |
|     const LAYOUT_TABS = 2;
 | |
| 
 | |
|     /**
 | |
|      * Known navigation types
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     protected static $types;
 | |
| 
 | |
|     /**
 | |
|      * This navigation's items
 | |
|      *
 | |
|      * @var NavigationItem[]
 | |
|      */
 | |
|     protected $items = array();
 | |
| 
 | |
|     /**
 | |
|      * This navigation's layout
 | |
|      *
 | |
|      * @var int
 | |
|      */
 | |
|     protected $layout;
 | |
| 
 | |
|     public function offsetExists($offset): bool
 | |
|     {
 | |
|         return isset($this->items[$offset]);
 | |
|     }
 | |
| 
 | |
|     public function offsetGet($offset): ?NavigationItem
 | |
|     {
 | |
|         return $this->items[$offset] ?? null;
 | |
|     }
 | |
| 
 | |
|     public function offsetSet($offset, $value): void
 | |
|     {
 | |
|         $this->items[$offset] = $value;
 | |
|     }
 | |
| 
 | |
|     public function offsetUnset($offset): void
 | |
|     {
 | |
|         unset($this->items[$offset]);
 | |
|     }
 | |
| 
 | |
|     public function count(): int
 | |
|     {
 | |
|         return count($this->items);
 | |
|     }
 | |
| 
 | |
|     public function getIterator(): Traversable
 | |
|     {
 | |
|         $this->order();
 | |
|         return new ArrayIterator($this->items);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Create and return a new navigation item for the given configuration
 | |
|      *
 | |
|      * @param   string              $name
 | |
|      * @param   array|ConfigObject  $properties
 | |
|      *
 | |
|      * @return  NavigationItem
 | |
|      *
 | |
|      * @throws  InvalidArgumentException    If the $properties argument is neither an array nor a ConfigObject
 | |
|      */
 | |
|     public function createItem($name, $properties)
 | |
|     {
 | |
|         if ($properties instanceof ConfigObject) {
 | |
|             $properties = $properties->toArray();
 | |
|         } elseif (! is_array($properties)) {
 | |
|             throw new InvalidArgumentException('Argument $properties must be of type array or ConfigObject');
 | |
|         }
 | |
| 
 | |
|         $itemType = isset($properties['type']) ? StringHelper::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) {
 | |
|             if ($itemType !== 'MenuItem') {
 | |
|                 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;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Add a navigation item
 | |
|      *
 | |
|      * 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
 | |
|      *
 | |
|      * @return  NavigationItem|mixed
 | |
|      */
 | |
|     public function getItem($name, $default = null)
 | |
|     {
 | |
|         return isset($this->items[$name]) ? $this->items[$name] : $default;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return the currently active item or the first one if none is active
 | |
|      *
 | |
|      * @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()
 | |
|     {
 | |
|         return $this->items;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return whether this navigation is empty
 | |
|      *
 | |
|      * @return  bool
 | |
|      */
 | |
|     public function isEmpty()
 | |
|     {
 | |
|         return empty($this->items);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return whether this navigation has any renderable items
 | |
|      *
 | |
|      * @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()
 | |
|     {
 | |
|         return $this->layout;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set this navigation's layout
 | |
|      *
 | |
|      * @param   int     $layout
 | |
|      *
 | |
|      * @return  $this
 | |
|      */
 | |
|     public function setLayout($layout)
 | |
|     {
 | |
|         $this->layout = (int) $layout;
 | |
|         return $this;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Create and return the renderer for this navigation
 | |
|      *
 | |
|      * @return  RecursiveNavigationRenderer
 | |
|      */
 | |
|     public function getRenderer()
 | |
|     {
 | |
|         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
 | |
|      */
 | |
|     public 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)
 | |
|     {
 | |
|         $user = Auth::getInstance()->getUser();
 | |
|         if ($type !== 'dashboard-pane') {
 | |
|             // Shareables
 | |
|             $this->merge(Icinga::app()->getSharedNavigation($type));
 | |
| 
 | |
|             // User Preferences
 | |
|             $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;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return the global navigation item type configuration
 | |
|      *
 | |
|      * @return  array
 | |
|      */
 | |
|     public static function getItemTypeConfiguration()
 | |
|     {
 | |
|         $defaultItemTypes = array(
 | |
|             'menu-item' => array(
 | |
|                 'label'     => t('Menu Entry'),
 | |
|                 'config'    => 'menu'
 | |
|             )/*, // Disabled, until it is able to fully replace the old implementation
 | |
|             'dashlet'   => array(
 | |
|                 'label'     => 'Dashlet',
 | |
|                 'config'    => 'dashboard'
 | |
|             )*/
 | |
|         );
 | |
| 
 | |
|         $moduleItemTypes = array();
 | |
|         $moduleManager = Icinga::app()->getModuleManager();
 | |
|         foreach ($moduleManager->getLoadedModules() as $module) {
 | |
|             if (Auth::getInstance()->hasPermission($moduleManager::MODULE_PERMISSION_NS . $module->getName())) {
 | |
|                 foreach ($module->getNavigationItems() as $type => $options) {
 | |
|                     if (! isset($moduleItemTypes[$type])) {
 | |
|                         $moduleItemTypes[$type] = $options;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return array_merge($defaultItemTypes, $moduleItemTypes);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Create and return a new set of navigation items for the given configuration
 | |
|      *
 | |
|      * 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((string) $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);
 | |
|         }
 | |
|     }
 | |
| }
 |