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; 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; } }