summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Web/Navigation/Navigation.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Web/Navigation/Navigation.php')
-rw-r--r--library/Icinga/Web/Navigation/Navigation.php572
1 files changed, 572 insertions, 0 deletions
diff --git a/library/Icinga/Web/Navigation/Navigation.php b/library/Icinga/Web/Navigation/Navigation.php
new file mode 100644
index 0000000..4343c3c
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Navigation.php
@@ -0,0 +1,572 @@
+<?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;
+ $classPath = 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+$~', (int) $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);
+ }
+ }
+}