diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:43:12 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:43:12 +0000 |
commit | cd989f9c3aff968e19a3aeabc4eb9085787a6673 (patch) | |
tree | fbff2135e7013f196b891bbde54618eb050e4aaf /library/Director/Web/Controller/ObjectController.php | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-director-upstream.tar.xz icingaweb2-module-director-upstream.zip |
Adding upstream version 1.10.2.upstream/1.10.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Director/Web/Controller/ObjectController.php')
-rw-r--r-- | library/Director/Web/Controller/ObjectController.php | 733 |
1 files changed, 733 insertions, 0 deletions
diff --git a/library/Director/Web/Controller/ObjectController.php b/library/Director/Web/Controller/ObjectController.php new file mode 100644 index 0000000..0c06937 --- /dev/null +++ b/library/Director/Web/Controller/ObjectController.php @@ -0,0 +1,733 @@ +<?php + +namespace Icinga\Module\Director\Web\Controller; + +use gipfl\Web\Widget\Hint; +use Icinga\Exception\IcingaException; +use Icinga\Exception\InvalidPropertyException; +use Icinga\Exception\NotFoundError; +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Db\Branch\BranchedObject; +use Icinga\Module\Director\Db\Branch\UuidLookup; +use Icinga\Module\Director\Deployment\DeploymentInfo; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\NestingError; +use Icinga\Module\Director\Forms\DeploymentLinkForm; +use Icinga\Module\Director\Forms\IcingaCloneObjectForm; +use Icinga\Module\Director\Forms\IcingaObjectFieldForm; +use Icinga\Module\Director\Objects\IcingaCommand; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\IcingaObjectGroup; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Objects\IcingaServiceSet; +use Icinga\Module\Director\RestApi\IcingaObjectHandler; +use Icinga\Module\Director\Web\Controller\Extension\ObjectRestrictions; +use Icinga\Module\Director\Web\Form\DirectorObjectForm; +use Icinga\Module\Director\Web\ObjectPreview; +use Icinga\Module\Director\Web\Table\ActivityLogTable; +use Icinga\Module\Director\Web\Table\BranchActivityTable; +use Icinga\Module\Director\Web\Table\GroupMemberTable; +use Icinga\Module\Director\Web\Table\IcingaObjectDatafieldTable; +use Icinga\Module\Director\Web\Tabs\ObjectTabs; +use Icinga\Module\Director\Web\Widget\BranchedObjectHint; +use gipfl\IcingaWeb2\Link; +use ipl\Html\Html; +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; + +abstract class ObjectController extends ActionController +{ + use ObjectRestrictions; + use BranchHelper; + + /** @var IcingaObject */ + protected $object; + + /** @var bool This controller handles REST API requests */ + protected $isApified = true; + + /** @var array Allowed object types we are allowed to edit anyways */ + protected $allowedExternals = array( + 'apiuser', + 'endpoint' + ); + + protected $type; + + /** @var string|null */ + protected $objectBaseUrl; + + public function init() + { + parent::init(); + $this->enableStaticObjectLoader($this->getTableName()); + if ($this->getRequest()->isApiRequest()) { + $this->initializeRestApi(); + } else { + $this->initializeWebRequest(); + } + } + + protected function initializeRestApi() + { + $handler = new IcingaObjectHandler($this->getRequest(), $this->getResponse(), $this->db()); + try { + $this->loadOptionalObject(); + } catch (NotFoundError $e) { + // Silently ignore the error, the handler will complain + $handler->sendJsonError($e, 404); + // TODO: nice shutdown + exit; + } + + $handler->setApi($this->api()); + if ($this->object) { + $handler->setObject($this->object); + } + $handler->dispatch(); + // Hint: also here, hard exit. There is too much magic going on. + // Letting this bubble up smoothly would be "correct", but proved + // to be too fragile. Web 2, all kinds of pre/postDispatch magic, + // different view renderers - hard exit is the only safe bet right + // now. + exit; + } + + protected function initializeWebRequest() + { + $this->loadOptionalObject(); + if ($this->getRequest()->getActionName() === 'add') { + $this->addSingleTab( + sprintf($this->translate('Add %s'), ucfirst($this->getType())), + null, + 'add' + ); + } else { + $this->tabs(new ObjectTabs( + $this->getRequest()->getControllerName(), + $this->getAuth(), + $this->object + )); + } + if ($this->object !== null) { + $this->addDeploymentLink(); + } + } + + /** + * @throws NotFoundError + */ + public function indexAction() + { + if (! $this->getRequest()->isApiRequest()) { + $this->redirectToPreviewForExternals() + ->editAction(); + } + } + + public function addAction() + { + $this->tabs()->activate('add'); + $url = sprintf('director/%ss', $this->getPluralType()); + + $imports = $this->params->get('imports'); + $form = $this->loadObjectForm() + ->presetImports($imports) + ->setSuccessUrl($url); + + if ($oType = $this->params->get('type', 'object')) { + $form->setPreferredObjectType($oType); + } + if ($oType === 'template') { + if ($this->showNotInBranch($this->translate('Creating Templates'))) { + $this->addTitle($this->translate('Create a new Template')); + return; + } + + $this->addTemplate(); + } else { + $this->addObject(); + } + $branch = $this->getBranch(); + if ($branch->isBranch() && ! $this->getRequest()->isApiRequest()) { + $this->content()->add(new BranchedObjectHint($branch, $this->Auth())); + } + + $form->handleRequest(); + $this->content()->add($form); + } + + /** + * @throws NotFoundError + */ + public function editAction() + { + $object = $this->requireObject(); + $this->tabs()->activate('modify'); + $this->addObjectTitle(); + // Hint: Service Sets are 'templates' (as long as not being assigned to a host + if ($this->getTableName() !== 'icinga_service_set' + && $object->isTemplate() + && $this->showNotInBranch($this->translate('Modifying Templates')) + ) { + return; + } + if ($object->isApplyRule() && $this->showNotInBranch($this->translate('Modifying Apply Rules'))) { + return; + } + + $this->addObjectForm($object) + ->addActionClone() + ->addActionUsage() + ->addActionBasket(); + } + + /** + * @throws NotFoundError + * @throws \Icinga\Security\SecurityException + */ + public function renderAction() + { + $this->assertTypePermission() + ->assertPermission('director/showconfig'); + $this->tabs()->activate('render'); + $preview = new ObjectPreview($this->requireObject(), $this->getRequest()); + if ($this->object->isExternal()) { + $this->addActionClone(); + } + $this->addActionBasket(); + $preview->renderTo($this); + } + + /** + * @throws NotFoundError + */ + public function cloneAction() + { + $this->assertTypePermission(); + $object = $this->requireObject(); + $this->addTitle($this->translate('Clone: %s'), $object->getObjectName()) + ->addBackToObjectLink(); + + if ($object->isTemplate() && $this->showNotInBranch($this->translate('Cloning Templates'))) { + return; + } + + if ($object->isTemplate() && $this->showNotInBranch($this->translate('Cloning Apply Rules'))) { + return; + } + + $form = IcingaCloneObjectForm::load() + ->setBranch($this->getBranch()) + ->setObject($object) + ->setObjectBaseUrl($this->getObjectBaseUrl()) + ->handleRequest(); + + if ($object->isExternal()) { + $this->tabs()->activate('render'); + } else { + $this->tabs()->activate('modify'); + } + $this->content()->add($form); + } + + /** + * @throws NotFoundError + * @throws \Icinga\Security\SecurityException + */ + public function fieldsAction() + { + $this->assertPermission('director/admin'); + $object = $this->requireObject(); + $type = $this->getType(); + + $this->addTitle( + $this->translate('Custom fields: %s'), + $object->getObjectName() + ); + $this->tabs()->activate('fields'); + if ($this->showNotInBranch($this->translate('Managing Fields'))) { + return; + } + + try { + $this->addFieldsFormAndTable($object, $type); + } catch (NestingError $e) { + $this->content()->add(Hint::error($e->getMessage())); + } + } + + protected function addFieldsFormAndTable($object, $type) + { + $form = IcingaObjectFieldForm::load() + ->setDb($this->db()) + ->setIcingaObject($object); + + if ($id = $this->params->get('field_id')) { + $form->loadObject([ + "${type}_id" => $object->id, + 'datafield_id' => $id + ]); + + $this->actions()->add(Link::create( + $this->translate('back'), + $this->url()->without('field_id'), + null, + ['class' => 'icon-left-big'] + )); + } + $form->handleRequest(); + $this->content()->add($form); + $table = new IcingaObjectDatafieldTable($object); + $table->getAttributes()->set('data-base-target', '_self'); + $table->renderTo($this); + } + + /** + * @throws NotFoundError + * @throws \Icinga\Security\SecurityException + */ + public function historyAction() + { + $this + ->assertTypePermission() + ->assertPermission('director/audit') + ->setAutorefreshInterval(10) + ->tabs()->activate('history'); + + $name = $this->requireObject()->getObjectName(); + $this->addTitle($this->translate('Activity Log: %s'), $name); + + $db = $this->db(); + $objectTable = $this->object->getTableName(); + $table = (new ActivityLogTable($db)) + ->setLastDeployedId($db->getLastDeploymentActivityLogId()) + ->filterObject($objectTable, $name); + if ($host = $this->params->get('host')) { + $table->filterHost($host); + } + $this->showOptionalBranchActivity($table); + $table->renderTo($this); + } + + /** + * @throws NotFoundError + */ + public function membershipAction() + { + $object = $this->requireObject(); + if (! $object instanceof IcingaObjectGroup) { + throw new NotFoundError('Not Found'); + } + + $this + ->addTitle($this->translate('Group membership: %s'), $object->getObjectName()) + ->setAutorefreshInterval(15) + ->tabs()->activate('membership'); + + $type = substr($this->getType(), 0, -5); + GroupMemberTable::create($type, $this->db()) + ->setGroup($object) + ->renderTo($this); + } + + /** + * @return $this + * @throws NotFoundError + */ + protected function addObjectTitle() + { + $object = $this->requireObject(); + $name = $object->getObjectName(); + if ($object->isTemplate()) { + $this->addTitle($this->translate('Template: %s'), $name); + } else { + $this->addTitle($name); + } + + return $this; + } + + /** + * @return $this + * @throws NotFoundError + */ + protected function addActionUsage() + { + $type = $this->getType(); + $object = $this->requireObject(); + if ($object->isTemplate() && $type !== 'serviceSet') { + $this->actions()->add([ + Link::create( + $this->translate('Usage'), + "director/${type}template/usage", + ['name' => $object->getObjectName()], + ['class' => 'icon-sitemap'] + ) + ]); + } + + return $this; + } + + protected function addActionClone() + { + $this->actions()->add(Link::create( + $this->translate('Clone'), + $this->getObjectBaseUrl() . '/clone', + $this->object->getUrlParams(), + array('class' => 'icon-paste') + )); + + return $this; + } + + /** + * @return $this + */ + protected function addActionBasket() + { + if ($this->hasBasketSupport()) { + $object = $this->object; + if ($object instanceof ExportInterface) { + if ($object instanceof IcingaCommand) { + if ($object->isExternal()) { + $type = 'ExternalCommand'; + } elseif ($object->isTemplate()) { + $type = 'CommandTemplate'; + } else { + $type = 'Command'; + } + } elseif ($object instanceof IcingaServiceSet) { + $type = 'ServiceSet'; + } elseif ($object->isTemplate()) { + $type = ucfirst($this->getType()) . 'Template'; + } elseif ($object->isGroup()) { + $type = ucfirst($this->getType()); + } else { + // Command? Sure? + $type = ucfirst($this->getType()); + } + $this->actions()->add(Link::create( + $this->translate('Add to Basket'), + 'director/basket/add', + [ + 'type' => $type, + 'names' => $object->getUniqueIdentifier() + ], + ['class' => 'icon-tag'] + )); + } + } + + return $this; + } + + protected function addTemplate() + { + $this->assertPermission('director/admin'); + $this->addTitle( + $this->translate('Add new Icinga %s template'), + $this->getTranslatedType() + ); + } + + protected function addObject() + { + $this->assertTypePermission(); + $imports = $this->params->get('imports'); + if (is_string($imports) && strlen($imports)) { + $this->addTitle( + $this->translate('Add %s: %s'), + $this->getTranslatedType(), + $imports + ); + } else { + $this->addTitle( + $this->translate('Add new Icinga %s'), + $this->getTranslatedType() + ); + } + } + + protected function redirectToPreviewForExternals() + { + if ($this->object + && $this->object->isExternal() + && ! in_array($this->object->getShortTableName(), $this->allowedExternals) + ) { + $this->redirectNow( + $this->getRequest()->getUrl()->setPath(sprintf('director/%s/render', $this->getType())) + ); + } + + return $this; + } + + protected function getType() + { + if ($this->type === null) { + // Strip final 's' and upcase an eventual 'group' + $this->type = preg_replace( + array('/group$/', '/period$/', '/argument$/', '/apiuser$/', '/set$/'), + array('Group', 'Period', 'Argument', 'ApiUser', 'Set'), + $this->getRequest()->getControllerName() + ); + } + + return $this->type; + } + + protected function getPluralType() + { + return $this->getType() . 's'; + } + + protected function getTranslatedType() + { + return $this->translate(ucfirst($this->getType())); + } + + protected function assertTypePermission() + { + $type = strtolower($this->getPluralType()); + // TODO: Check getPluralType usage, fix it there. + if ($type === 'scheduleddowntimes') { + $type = 'scheduled-downtimes'; + } + + return $this->assertPermission("director/$type"); + } + + protected function loadOptionalObject() + { + if ($this->params->get('uuid') || null !== $this->params->get('name') || $this->params->get('id')) { + $this->loadObject(); + } + } + + /** + * @return ?UuidInterface + * @throws InvalidPropertyException + * @throws NotFoundError + */ + protected function getUuidFromUrl() + { + $key = null; + if ($uuid = $this->params->get('uuid')) { + $key = Uuid::fromString($uuid); + } elseif ($id = $this->params->get('id')) { + $key = (int) $id; + } elseif (null !== ($name = $this->params->get('name'))) { + $key = $name; + } + if ($key === null) { + $request = $this->getRequest(); + if ($request->isApiRequest() && $request->isGet()) { + $this->getResponse()->setHttpResponseCode(422); + + throw new InvalidPropertyException( + 'Cannot load object, missing parameters' + ); + } + + return null; + } + + return $this->requireUuid($key); + } + + protected function loadObject() + { + if ($this->object) { + throw new ProgrammingError('Loading an object twice is not very efficient'); + } + + $this->object = $this->loadSpecificObject($this->getTableName(), $this->getUuidFromUrl(), true); + } + + protected function loadSpecificObject($tableName, $key, $showHint = false) + { + $branch = $this->getBranch(); + $branchedObject = BranchedObject::load($this->db(), $tableName, $key, $branch); + $object = $branchedObject->getBranchedDbObject($this->db()); + assert($object instanceof IcingaObject); + $object->setBeingLoadedFromDb(); + if (! $this->allowsObject($object)) { + throw new NotFoundError('No such object available'); + } + if ($showHint && $branch->isBranch() && $object->isObject() && ! $this->getRequest()->isApiRequest()) { + $this->content()->add(new BranchedObjectHint($branch, $this->Auth(), $branchedObject)); + } + + return $object; + } + + protected function requireUuid($key) + { + if (! $key instanceof UuidInterface) { + $key = UuidLookup::findUuidForKey($key, $this->getTableName(), $this->db(), $this->getBranch()); + if ($key === null) { + throw new NotFoundError('No such object available'); + } + } + + return $key; + } + + protected function getTableName() + { + return DbObjectTypeRegistry::tableNameByType($this->getType()); + } + + protected function addDeploymentLink() + { + try { + $info = new DeploymentInfo($this->db()); + $info->setObject($this->object); + + if (! $this->getRequest()->isApiRequest()) { + if ($this->getBranch()->isBranch()) { + $this->actions()->add($this->linkToMergeBranch($this->getBranch())); + } else { + $this->actions()->add( + DeploymentLinkForm::create( + $this->db(), + $info, + $this->Auth(), + $this->api() + )->handleRequest() + ); + } + } + } catch (IcingaException $e) { + // pass (deployment may not be set up yet) + } + } + + protected function linkToMergeBranch(Branch $branch) + { + $link = Branch::requireHook()->linkToBranch($branch, $this->Auth(), $this->translate('Merge')); + if ($link instanceof Link) { + $link->addAttributes(['class' => 'icon-flapping']); + } + + return $link; + } + + protected function addBackToObjectLink() + { + $params = [ + 'uuid' => $this->object->getUniqueId()->toString(), + ]; + + if ($this->object instanceof IcingaService) { + if (($host = $this->object->get('host')) !== null) { + $params['host'] = $host; + } elseif (($set = $this->object->get('service_set')) !== null) { + $params['set'] = $set; + } + } + + $this->actions()->add(Link::create( + $this->translate('back'), + $this->getObjectBaseUrl(), + $params, + ['class' => 'icon-left-big'] + )); + + return $this; + } + + protected function addObjectForm(IcingaObject $object = null) + { + $form = $this->loadObjectForm($object); + $this->content()->add($form); + $form->handleRequest(); + return $this; + } + + protected function loadObjectForm(IcingaObject $object = null) + { + /** @var DirectorObjectForm $class */ + $class = sprintf( + 'Icinga\\Module\\Director\\Forms\\Icinga%sForm', + ucfirst($this->getType()) + ); + + $form = $class::load() + ->setDb($this->db()) + ->setAuth($this->Auth()); + + if ($object !== null) { + $form->setObject($object); + } + if (true || $form->supportsBranches()) { + $form->setBranch($this->getBranch()); + } + + $this->onObjectFormLoaded($form); + + return $form; + } + + protected function getObjectBaseUrl() + { + return $this->objectBaseUrl ?: 'director/' . strtolower($this->getType()); + } + + protected function hasBasketSupport() + { + return $this->object->isTemplate() || $this->object->isGroup(); + } + + protected function onObjectFormLoaded(DirectorObjectForm $form) + { + } + + /** + * @return IcingaObject + * @throws NotFoundError + */ + protected function requireObject() + { + if (! $this->object) { + $this->getResponse()->setHttpResponseCode(404); + if (null === $this->params->get('name')) { + throw new NotFoundError('You need to pass a "name" parameter to access a specific object'); + } else { + throw new NotFoundError('No such object available'); + } + } + + return $this->object; + } + + protected function showOptionalBranchActivity($activityTable) + { + $branch = $this->getBranch(); + if ($branch->isBranch() && (int) $this->params->get('page', '1') === 1) { + $table = new BranchActivityTable($branch->getUuid(), $this->db(), $this->object->getUniqueId()); + if (count($table) > 0) { + $this->content()->add(Hint::info(Html::sprintf($this->translate( + 'The following modifications are visible in this %s only...' + ), Branch::requireHook()->linkToBranch( + $branch, + $this->Auth(), + $this->translate('configuration branch') + )))); + $this->content()->add($table); + if (count($activityTable) === 0) { + return; + } + $this->content()->add(Html::tag('br')); + $this->content()->add(Hint::ok($this->translate( + '...and the modifications below are already in the main branch:' + ))); + $this->content()->add(Html::tag('br')); + } + } + } +} |