From 3e02d5aff85babc3ffbfcf52313f2108e313aa23 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 13 Apr 2024 13:46:43 +0200 Subject: Adding upstream version 2.12.1. Signed-off-by: Daniel Baumann --- application/forms/Navigation/DashletForm.php | 35 + application/forms/Navigation/MenuItemForm.php | 31 + .../forms/Navigation/NavigationConfigForm.php | 853 +++++++++++++++++++++ .../forms/Navigation/NavigationItemForm.php | 114 +++ 4 files changed, 1033 insertions(+) create mode 100644 application/forms/Navigation/DashletForm.php create mode 100644 application/forms/Navigation/MenuItemForm.php create mode 100644 application/forms/Navigation/NavigationConfigForm.php create mode 100644 application/forms/Navigation/NavigationItemForm.php (limited to 'application/forms/Navigation') diff --git a/application/forms/Navigation/DashletForm.php b/application/forms/Navigation/DashletForm.php new file mode 100644 index 0000000..6575fd7 --- /dev/null +++ b/application/forms/Navigation/DashletForm.php @@ -0,0 +1,35 @@ +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)' + ) + ) + ); + } +} diff --git a/application/forms/Navigation/MenuItemForm.php b/application/forms/Navigation/MenuItemForm.php new file mode 100644 index 0000000..c9fa729 --- /dev/null +++ b/application/forms/Navigation/MenuItemForm.php @@ -0,0 +1,31 @@ +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' + )); + } + } +} diff --git a/application/forms/Navigation/NavigationConfigForm.php b/application/forms/Navigation/NavigationConfigForm.php new file mode 100644 index 0000000..0c4ae32 --- /dev/null +++ b/application/forms/Navigation/NavigationConfigForm.php @@ -0,0 +1,853 @@ +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 + * + * @param string $type + * + * @return Config + */ + public function getUserConfig($type = null) + { + if ($this->userConfig === null || $type !== null) { + 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; + } + + /** + * 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 + * + * @param string $type + * + * @return Config + */ + 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; + } + + /** + * 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($type) as $sectionName => $sectionConfig) { + if ((string) $sectionName !== $this->itemToLoad + && $sectionConfig->owner === ($owner ?: $this->getUser()->getUsername()) + && ! in_array($sectionName, $children, true) + ) { + $names[] = $sectionName; + } + } + + foreach ($this->getUserConfig($type) as $sectionName => $sectionConfig) { + if ((string) $sectionName !== $this->itemToLoad + && ! 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 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($data['type']); + if ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) { + if ($this->getUser()->can('user/share/navigation')) { + $data['owner'] = $this->getUser()->getUsername(); + $config = $this->getShareConfig($data['type']); + $shared = true; + } else { + unset($data['users']); + unset($data['groups']); + } + } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'], $data['type'])) { + $data['owner'] = $this->getUser()->getUsername(); + $config = $this->getShareConfig($data['type']); + $shared = true; + } + + $itemName = $data['name']; + $exists = $config->hasSection($itemName); + if (! $exists) { + if ($shared) { + $exists = $this->getUserConfig($data['type'])->hasSection($itemName); + } else { + $exists = (bool) $this->getShareConfig($data['type']) + ->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('user/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 { + $exists = Config::navigation($itemConfig->type, $ownerName)->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); + + 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 { + $config = Config::navigation($itemConfig->type, $itemConfig->owner); + } + + 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); + if ($itemType === null) { + throw new ProgrammingError( + 'This should actually not happen. Create a bug report at https://github.com/icinga/icingaweb2' + . ' or remove this assertion if you know what you\'re doing' + ); + } + + $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('user/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( + 'textarea', + 'users', + array( + 'label' => $this->translate('Users'), + 'description' => $this->translate( + 'Comma separated list of usernames to share this item with' + ) + ) + ); + $this->addElement( + 'textarea', + 'groups', + array( + 'label' => $this->translate('Groups'), + 'description' => $this->translate( + 'Comma separated list of group names to share this item with' + ) + ) + ); + } + } + + 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)) { + $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' => ['' => $this->translate('None', 'No parent for a navigation item')] + + (empty($availableParents) ? [] : array_combine($availableParents, $availableParents)) + ) + ); + } else { + $this->addElement('hidden', 'parent', ['disabled' => true]); + } + + $this->addSubForm($itemForm, 'item_form'); + $itemForm->create($formData); // May require a parent which gets set by addSubForm() + } + + /** + * DO NOT USE! This will be removed soon, very soon... + */ + public function setDefaultUrl($url) + { + $this->defaultUrl = $url; + } + + /** + * 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); + } elseif ($this->defaultUrl !== null) { + $this->populate(array('url' => $this->defaultUrl)); + } + } + + /** + * {@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( + sprintf( + $this->translate( + '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( + sprintf( + $this->translate( + 'You are not permitted to share this navigation item with the following groups: %s' + ), + implode(', ', $forbiddenGroups) + ) + ); + } + } + } + + return $valid; + } + + /** + * {@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('user/share/navigation') + ) { + return $this->getShareConfig(); + } + } + } + + /** + * Return whether the given navigation item has been shared + * + * @param string $name + * @param string $type + * + * @return bool + */ + protected function hasBeenShared($name, $type = null) + { + return $this->getShareConfig($type) === $this->getConfigForItem($name); + } + + /** + * Return the form for the given type of navigation item + * + * @param string $type + * + * @return Form + */ + protected function getItemForm($type) + { + $className = StringHelper::cname($type, '-') . 'Form'; + + $form = null; + $classPath = 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; + } +} diff --git a/application/forms/Navigation/NavigationItemForm.php b/application/forms/Navigation/NavigationItemForm.php new file mode 100644 index 0000000..6cf15e7 --- /dev/null +++ b/application/forms/Navigation/NavigationItemForm.php @@ -0,0 +1,114 @@ +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( + 'textarea', + 'url', + array( + 'allowEmpty' => true, + 'label' => $this->translate('Url'), + 'description' => $this->translate( + 'The url of this navigation item. Leave blank if only the name should be displayed.' + . ' For urls with username and password and for all external urls,' + . ' make sure to prepend an appropriate protocol identifier (e.g. http://example.tld)' + ), + 'validators' => array( + array( + 'Callback', + false, + array( + 'callback' => function ($url) { + // Matches if the given url contains obviously + // a username but not any protocol identifier + return !preg_match('#^((?=[^/@]).)+@.*$#', $url); + }, + 'messages' => array( + 'callbackValue' => $this->translate( + 'Missing protocol identifier' + ) + ) + ) + ) + ) + ) + ); + + $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' + ) + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getValues($suppressArrayNotation = false) + { + $values = parent::getValues($suppressArrayNotation); + // The regex here specifically matches the port-macro as it's the only one preventing Url::fromPath() from + // successfully parsing the given url. Any other macro such as for the scheme or host simply gets identified + // as path which is just fine in this case. + if (isset($values['url']) && $values['url'] && !preg_match('~://.+:\d*?(\$.+\$)~', $values['url'])) { + $url = Url::fromPath($values['url']); + if ($url->getBasePath() === $this->getRequest()->getBasePath()) { + $values['url'] = $url->getRelativeUrl(); + } else { + $values['url'] = $url->getAbsoluteUrl(); + } + } + + return $values; + } +} -- cgit v1.2.3