diff --git a/application/forms/Command/CommandForm.php b/application/forms/Command/CommandForm.php index b16b5e862..a535c6d69 100644 --- a/application/forms/Command/CommandForm.php +++ b/application/forms/Command/CommandForm.php @@ -28,6 +28,9 @@ abstract class CommandForm extends Form /** @var mixed */ protected $objects; + /** @var bool */ + protected $isApiTarget = false; + /** * Whether an error occurred while sending the command * @@ -61,6 +64,30 @@ public function getObjects() return $this->objects; } + /** + * Set whether this form is an API target + * + * @param bool $state + * + * @return $this + */ + public function setIsApiTarget(bool $state = true): self + { + $this->isApiTarget = $state; + + return $this; + } + + /** + * Get whether this form is an API target + * + * @return bool + */ + public function isApiTarget(): bool + { + return $this->isApiTarget; + } + /** * Create and add form elements representing the command's options * @@ -87,8 +114,11 @@ abstract protected function getCommands(Traversable $objects): Traversable; protected function assemble() { $this->assembleElements(); - $this->assembleSubmitButton(); - $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId())); + + if (! $this->isApiTarget()) { + $this->assembleSubmitButton(); + $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId())); + } } protected function onSuccess() diff --git a/library/Icingadb/Common/CommandActions.php b/library/Icingadb/Common/CommandActions.php index 56a75e302..2cd13feef 100644 --- a/library/Icingadb/Common/CommandActions.php +++ b/library/Icingadb/Common/CommandActions.php @@ -4,7 +4,6 @@ namespace Icinga\Module\Icingadb\Common; -use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Module\Icingadb\Forms\Command\Object\AcknowledgeProblemForm; use Icinga\Module\Icingadb\Forms\Command\Object\AddCommentForm; @@ -17,6 +16,7 @@ use Icinga\Module\Icingadb\Forms\Command\Object\SendCustomNotificationForm; use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; use Icinga\Security\SecurityException; +use Icinga\Web\Notification; use ipl\Orm\Model; use ipl\Orm\Query; use ipl\Web\Url; @@ -138,11 +138,35 @@ protected function assertIsGrantedOnCommandTargets(string $permission) */ protected function handleCommandForm($form) { + $isXhr = $this->getRequest()->isXmlHttpRequest(); + if ($isXhr && $this->getRequest()->isApiRequest()) { + // Prevents the framework already, this is just a fail-safe + $this->httpBadRequest('Responding with JSON during a Web request is not supported'); + } + if (is_string($form)) { - /** @var \Icinga\Module\Icingadb\Forms\Command\CommandForm $form */ + /** @var CommandForm $form */ $form = new $form(); } + $form->setObjects($this->getCommandTargets()); + + if ($isXhr) { + $this->handleWebRequest($form); + } else { + $this->handleApiRequest($form); + } + } + + /** + * Handle a Web request for the given form + * + * @param CommandForm $form + * + * @return void + */ + protected function handleWebRequest(CommandForm $form): void + { $actionUrl = $this->getRequest()->getUrl(); if ($this->view->compact) { $actionUrl = clone $actionUrl; @@ -153,7 +177,6 @@ protected function handleCommandForm($form) } $form->setAction($actionUrl->getAbsoluteUrl()); - $form->setObjects($this->getCommandTargets()); $form->on($form::ON_SUCCESS, function () { // This forces the column to reload nearly instantly after the redirect // and ensures the effect of the command is visible to the user asap @@ -162,11 +185,41 @@ protected function handleCommandForm($form) $this->redirectNow($this->getCommandTargetsUrl()); }); - $form->handleRequest(ServerRequest::fromGlobals()); + $form->handleRequest($this->getServerRequest()); $this->addContent($form); } + /** + * Handle an API request for the given form + * + * @param CommandForm $form + * + * @return never + */ + protected function handleApiRequest(CommandForm $form) + { + $form->setIsApiTarget(); + $form->on($form::ON_SUCCESS, function () { + $this->getResponse() + ->json() + ->setSuccessData(Notification::getInstance()->popMessages()) + ->sendResponse(); + }); + + $form->handleRequest($this->getServerRequest()); + + $errors = []; + foreach ($form->getElements() as $element) { + $errors[$element->getName()] = $element->getMessages(); + } + + $response = $this->getResponse()->json(); + $response->setHttpResponseCode(422); + $response->setFailData($errors) + ->sendResponse(); + } + public function acknowledgeAction() { $this->assertIsGrantedOnCommandTargets('icingadb/command/acknowledge-problem');