summaryrefslogtreecommitdiffstats
path: root/application/forms
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:44:46 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:44:46 +0000
commitb18bc644404e02b57635bfcc8258e85abb141146 (patch)
tree686512eacb2dba0055277ef7ec2f28695b3418ea /application/forms
parentInitial commit. (diff)
downloadicingadb-web-b18bc644404e02b57635bfcc8258e85abb141146.tar.xz
icingadb-web-b18bc644404e02b57635bfcc8258e85abb141146.zip
Adding upstream version 1.1.1.upstream/1.1.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--application/forms/ApiTransportForm.php102
-rw-r--r--application/forms/Command/CommandForm.php179
-rw-r--r--application/forms/Command/Instance/ToggleInstanceFeaturesForm.php154
-rw-r--r--application/forms/Command/Object/AcknowledgeProblemForm.php210
-rw-r--r--application/forms/Command/Object/AddCommentForm.php162
-rw-r--r--application/forms/Command/Object/CheckNowForm.php72
-rw-r--r--application/forms/Command/Object/DeleteCommentForm.php75
-rw-r--r--application/forms/Command/Object/DeleteDowntimeForm.php90
-rw-r--r--application/forms/Command/Object/ProcessCheckResultForm.php156
-rw-r--r--application/forms/Command/Object/RemoveAcknowledgementForm.php77
-rw-r--r--application/forms/Command/Object/ScheduleCheckForm.php137
-rw-r--r--application/forms/Command/Object/ScheduleHostDowntimeForm.php119
-rw-r--r--application/forms/Command/Object/ScheduleServiceDowntimeForm.php267
-rw-r--r--application/forms/Command/Object/SendCustomNotificationForm.php125
-rw-r--r--application/forms/Command/Object/ToggleObjectFeaturesForm.php186
-rw-r--r--application/forms/DatabaseConfigForm.php33
-rw-r--r--application/forms/Navigation/ActionForm.php58
-rw-r--r--application/forms/Navigation/IcingadbHostActionForm.php10
-rw-r--r--application/forms/Navigation/IcingadbServiceActionForm.php10
-rw-r--r--application/forms/RedisConfigForm.php606
-rw-r--r--application/forms/SetAsBackendForm.php34
21 files changed, 2862 insertions, 0 deletions
diff --git a/application/forms/ApiTransportForm.php b/application/forms/ApiTransportForm.php
new file mode 100644
index 0000000..27c147b
--- /dev/null
+++ b/application/forms/ApiTransportForm.php
@@ -0,0 +1,102 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms;
+
+use Icinga\Data\ConfigObject;
+use Icinga\Module\Icingadb\Command\Transport\CommandTransport;
+use Icinga\Module\Icingadb\Command\Transport\CommandTransportException;
+use Icinga\Web\Session;
+use ipl\Web\Common\CsrfCounterMeasure;
+use ipl\Web\Compat\CompatForm;
+
+class ApiTransportForm extends CompatForm
+{
+ use CsrfCounterMeasure;
+
+ protected function assemble()
+ {
+ // TODO: Use a validator to check if a name is not already in use
+ $this->addElement('text', 'name', [
+ 'required' => true,
+ 'label' => t('Transport Name')
+ ]);
+
+ $this->addElement('hidden', 'transport', [
+ 'value' => 'api'
+ ]);
+
+ $this->addElement('text', 'host', [
+ 'required' => true,
+ 'id' => 'api_transport_host',
+ 'label' => t('Host'),
+ 'description' => t('Hostname or address of the Icinga master')
+ ]);
+
+ // TODO: Don't rely only on browser validation
+ $this->addElement('number', 'port', [
+ 'required' => true,
+ 'label' => t('Port'),
+ 'value' => 5665,
+ 'min' => 1,
+ 'max' => 65536
+ ]);
+
+ $this->addElement('text', 'username', [
+ 'required' => true,
+ 'label' => t('API Username'),
+ 'description' => t('User to authenticate with using HTTP Basic Auth')
+ ]);
+
+ $this->addElement('password', 'password', [
+ 'required' => true,
+ 'autocomplete' => 'new-password',
+ 'label' => t('API Password')
+ ]);
+
+ $this->addElement('submit', 'btn_submit', [
+ 'label' => t('Save')
+ ]);
+
+ $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId()));
+ }
+
+ public function validate()
+ {
+ parent::validate();
+ if (! $this->isValid) {
+ return $this;
+ }
+
+ if ($this->getPopulatedValue('force_creation') === 'y') {
+ return $this;
+ }
+
+ try {
+ CommandTransport::createTransport(new ConfigObject($this->getValues()))->probe();
+ } catch (CommandTransportException $e) {
+ $this->addMessage(
+ sprintf(t('Failed to successfully validate the configuration: %s'), $e->getMessage())
+ );
+
+ $forceCheckbox = $this->createElement(
+ 'checkbox',
+ 'force_creation',
+ [
+ 'ignore' => true,
+ 'label' => t('Force Changes'),
+ 'description' => t('Check this box to enforce changes without connectivity validation')
+ ]
+ );
+
+ $this->registerElement($forceCheckbox);
+ $this->decorate($forceCheckbox);
+ $this->prepend($forceCheckbox);
+
+ $this->isValid = false;
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Command/CommandForm.php b/application/forms/Command/CommandForm.php
new file mode 100644
index 0000000..a535c6d
--- /dev/null
+++ b/application/forms/Command/CommandForm.php
@@ -0,0 +1,179 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command;
+
+use ArrayIterator;
+use Exception;
+use Generator;
+use Icinga\Application\Logger;
+use Icinga\Module\Icingadb\Command\IcingaCommand;
+use Icinga\Module\Icingadb\Command\Transport\CommandTransport;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Web\Notification;
+use Icinga\Web\Session;
+use ipl\Html\Form;
+use ipl\Orm\Model;
+use ipl\Web\Common\CsrfCounterMeasure;
+use Traversable;
+
+abstract class CommandForm extends Form
+{
+ use Auth;
+ use CsrfCounterMeasure;
+
+ protected $defaultAttributes = ['class' => 'icinga-form icinga-controls'];
+
+ /** @var mixed */
+ protected $objects;
+
+ /** @var bool */
+ protected $isApiTarget = false;
+
+ /**
+ * Whether an error occurred while sending the command
+ *
+ * Prevents the success message from being rendered simultaneously
+ *
+ * @var bool
+ */
+ protected $errorOccurred = false;
+
+ /**
+ * Set the objects to issue the command for
+ *
+ * @param mixed $objects A traversable that is also countable
+ *
+ * @return $this
+ */
+ public function setObjects($objects): self
+ {
+ $this->objects = $objects;
+
+ return $this;
+ }
+
+ /**
+ * Get the objects to issue the command for
+ *
+ * @return mixed
+ */
+ 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
+ *
+ * @return void
+ */
+ abstract protected function assembleElements();
+
+ /**
+ * Create and add a submit button to the form
+ *
+ * @return void
+ */
+ abstract protected function assembleSubmitButton();
+
+ /**
+ * Get the commands to issue for the given objects
+ *
+ * @param Traversable<Model> $objects
+ *
+ * @return Traversable<IcingaCommand>
+ */
+ abstract protected function getCommands(Traversable $objects): Traversable;
+
+ protected function assemble()
+ {
+ $this->assembleElements();
+
+ if (! $this->isApiTarget()) {
+ $this->assembleSubmitButton();
+ $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId()));
+ }
+ }
+
+ protected function onSuccess()
+ {
+ $errors = [];
+ $objects = $this->getObjects();
+
+ foreach ($this->getCommands(is_array($objects) ? new ArrayIterator($objects) : $objects) as $command) {
+ try {
+ $this->sendCommand($command);
+ } catch (Exception $e) {
+ Logger::error($e->getMessage());
+ $errors[] = $e->getMessage();
+ }
+ }
+
+ if (! empty($errors)) {
+ if (count($errors) > 1) {
+ Notification::warning(
+ t('Some commands were not transmitted. Please check the log. The first error follows.')
+ );
+ }
+
+ $this->errorOccurred = true;
+
+ Notification::error($errors[0]);
+ }
+ }
+
+ /**
+ * Transmit the given command
+ *
+ * @param IcingaCommand $command
+ *
+ * @return void
+ */
+ protected function sendCommand(IcingaCommand $command)
+ {
+ (new CommandTransport())->send($command);
+ }
+
+ /**
+ * Yield the $objects the currently logged in user has the permission $permission for
+ *
+ * @param string $permission
+ * @param Traversable $objects
+ *
+ * @return Generator
+ */
+ protected function filterGrantedOn(string $permission, Traversable $objects): Generator
+ {
+ foreach ($objects as $object) {
+ if ($this->isGrantedOn($permission, $object)) {
+ yield $object;
+ }
+ }
+ }
+}
diff --git a/application/forms/Command/Instance/ToggleInstanceFeaturesForm.php b/application/forms/Command/Instance/ToggleInstanceFeaturesForm.php
new file mode 100644
index 0000000..cf14db8
--- /dev/null
+++ b/application/forms/Command/Instance/ToggleInstanceFeaturesForm.php
@@ -0,0 +1,154 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Instance;
+
+use Icinga\Module\Icingadb\Command\Instance\ToggleInstanceFeatureCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use Traversable;
+
+class ToggleInstanceFeaturesForm extends CommandForm
+{
+ protected $features;
+
+ protected $featureStatus;
+
+ /**
+ * ToggleFeature(s) being used to submit this form
+ *
+ * @var ToggleInstanceFeatureCommand[]
+ */
+ protected $submittedFeatures = [];
+
+ public function __construct(array $featureStatus)
+ {
+ $this->featureStatus = $featureStatus;
+ $this->features = [
+ ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS =>
+ t('Active Host Checks'),
+ ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS =>
+ t('Active Service Checks'),
+ ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS =>
+ t('Event Handlers'),
+ ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION =>
+ t('Flap Detection'),
+ ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS =>
+ t('Notifications'),
+ ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA =>
+ t('Performance Data')
+ ];
+
+ $this->getAttributes()->add('class', 'instance-features');
+
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ foreach ($this->submittedFeatures as $feature) {
+ $enabled = $feature->getEnabled();
+ switch ($feature->getFeature()) {
+ case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS:
+ if ($enabled) {
+ $message = t('Enabled active host checks successfully');
+ } else {
+ $message = t('Disabled active host checks successfully');
+ }
+
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS:
+ if ($enabled) {
+ $message = t('Enabled active service checks successfully');
+ } else {
+ $message = t('Disabled active service checks successfully');
+ }
+
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS:
+ if ($enabled) {
+ $message = t('Enabled event handlers successfully');
+ } else {
+ $message = t('Disabled event handlers checks successfully');
+ }
+
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION:
+ if ($enabled) {
+ $message = t('Enabled flap detection successfully');
+ } else {
+ $message = t('Disabled flap detection successfully');
+ }
+
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS:
+ if ($enabled) {
+ $message = t('Enabled notifications successfully');
+ } else {
+ $message = t('Disabled notifications successfully');
+ }
+
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA:
+ if ($enabled) {
+ $message = t('Enabled performance data successfully');
+ } else {
+ $message = t('Disabled performance data successfully');
+ }
+
+ break;
+ default:
+ $message = t('Invalid feature option');
+ break;
+ }
+
+ Notification::success($message);
+ }
+ });
+ }
+
+ protected function assembleElements()
+ {
+ $disabled = ! $this->getAuth()->hasPermission('icingadb/command/feature/instance');
+ $decorator = new IcingaFormDecorator();
+
+ foreach ($this->features as $feature => $label) {
+ $this->addElement(
+ 'checkbox',
+ $feature,
+ [
+ 'class' => 'autosubmit',
+ 'label' => $label,
+ 'disabled' => $disabled,
+ 'value' => (bool) $this->featureStatus[$feature]
+ ]
+ );
+ $decorator->decorate($this->getElement($feature));
+ }
+ }
+
+ protected function assembleSubmitButton()
+ {
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ foreach ($this->features as $feature => $spec) {
+ $featureState = $this->getElement($feature)->isChecked();
+
+ if ((int) $featureState === (int) $this->featureStatus[$feature]) {
+ continue;
+ }
+
+ $command = new ToggleInstanceFeatureCommand();
+ $command->setFeature($feature);
+ $command->setEnabled($featureState);
+
+ $this->submittedFeatures[] = $command;
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/AcknowledgeProblemForm.php b/application/forms/Command/Object/AcknowledgeProblemForm.php
new file mode 100644
index 0000000..81b93e2
--- /dev/null
+++ b/application/forms/Command/Object/AcknowledgeProblemForm.php
@@ -0,0 +1,210 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use DateInterval;
+use DateTime;
+use Icinga\Application\Config;
+use Icinga\Module\Icingadb\Command\Object\AcknowledgeProblemCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Web\Notification;
+use ipl\Html\Attributes;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Validator\CallbackValidator;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use ipl\Web\Widget\Icon;
+use Traversable;
+
+use function ipl\Stdlib\iterable_value_first;
+
+class AcknowledgeProblemForm extends CommandForm
+{
+ public function __construct()
+ {
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ $countObjects = count($this->getObjects());
+ if (iterable_value_first($this->getObjects()) instanceof Host) {
+ $message = sprintf(tp(
+ 'Acknowledged problem successfully',
+ 'Acknowledged problem on %d hosts successfully',
+ $countObjects
+ ), $countObjects);
+ } else {
+ $message = sprintf(tp(
+ 'Acknowledged problem successfully',
+ 'Acknowledged problem on %d services successfully',
+ $countObjects
+ ), $countObjects);
+ }
+
+ Notification::success($message);
+ });
+ }
+
+ protected function assembleElements()
+ {
+ $this->addHtml(new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'form-description']),
+ new Icon('info-circle', ['class' => 'form-description-icon']),
+ new HtmlElement(
+ 'ul',
+ null,
+ new HtmlElement('li', null, Text::create(t(
+ 'This command is used to acknowledge host or service problems. When a problem is acknowledged,'
+ . ' future notifications about problems are temporarily disabled until the host or service'
+ . ' recovers.'
+ )))
+ )
+ ));
+
+ $config = Config::module('icingadb');
+ $decorator = new IcingaFormDecorator();
+
+ $this->addElement(
+ 'textarea',
+ 'comment',
+ [
+ 'required' => true,
+ 'label' => t('Comment'),
+ 'description' => t(
+ 'If you work with other administrators, you may find it useful to share information about'
+ . ' the host or service that is having problems. Make sure you enter a brief description of'
+ . ' what you are doing.'
+ )
+ ]
+ );
+ $decorator->decorate($this->getElement('comment'));
+
+ $this->addElement(
+ 'checkbox',
+ 'persistent',
+ [
+ 'label' => t('Persistent Comment'),
+ 'value' => (bool) $config->get('settings', 'acknowledge_persistent', false),
+ 'description' => t(
+ 'If you want the comment to remain even when the acknowledgement is removed, check this'
+ . ' option.'
+ )
+ ]
+ );
+ $decorator->decorate($this->getElement('persistent'));
+
+ $this->addElement(
+ 'checkbox',
+ 'notify',
+ [
+ 'label' => t('Send Notification'),
+ 'value' => (bool) $config->get('settings', 'acknowledge_notify', true),
+ 'description' => t(
+ 'If you want an acknowledgement notification to be sent out to the appropriate contacts,'
+ . ' check this option.'
+ )
+ ]
+ );
+ $decorator->decorate($this->getElement('notify'));
+
+ $this->addElement(
+ 'checkbox',
+ 'sticky',
+ [
+ 'label' => t('Sticky Acknowledgement'),
+ 'value' => (bool) $config->get('settings', 'acknowledge_sticky', false),
+ 'description' => t(
+ 'If you want the acknowledgement to remain until the host or service recovers even if the host'
+ . ' or service changes state, check this option.'
+ )
+ ]
+ );
+ $decorator->decorate($this->getElement('sticky'));
+
+ $this->addElement(
+ 'checkbox',
+ 'expire',
+ [
+ 'ignore' => true,
+ 'class' => 'autosubmit',
+ 'value' => (bool) $config->get('settings', 'acknowledge_expire', false),
+ 'label' => t('Use Expire Time'),
+ 'description' => t('If the acknowledgement should expire, check this option.')
+ ]
+ );
+ $decorator->decorate($this->getElement('expire'));
+
+ if ($this->getElement('expire')->isChecked()) {
+ $expireTime = new DateTime();
+ $expireTime->add(new DateInterval($config->get('settings', 'acknowledge_expire_time', 'PT1H')));
+
+ $this->addElement(
+ 'localDateTime',
+ 'expire_time',
+ [
+ 'data-use-datetime-picker' => true,
+ 'required' => true,
+ 'value' => $expireTime,
+ 'label' => t('Expire Time'),
+ 'description' => t(
+ 'Choose the date and time when Icinga should delete the acknowledgement.'
+ ),
+ 'validators' => [
+ 'DateTime' => ['break_chain_on_failure' => true],
+ 'Callback' => function ($value, $validator) {
+ /** @var CallbackValidator $validator */
+ if ($value <= (new DateTime())) {
+ $validator->addMessage(t('The expire time must not be in the past'));
+ return false;
+ }
+
+ return true;
+ }
+ ]
+ ]
+ );
+ $decorator->decorate($this->getElement('expire_time'));
+ }
+ }
+
+ protected function assembleSubmitButton()
+ {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ [
+ 'required' => true,
+ 'label' => tp('Acknowledge problem', 'Acknowledge problems', count($this->getObjects()))
+ ]
+ );
+
+ (new IcingaFormDecorator())->decorate($this->getElement('btn_submit'));
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ $granted = $this->filterGrantedOn('icingadb/command/acknowledge-problem', $objects);
+
+ if ($granted->valid()) {
+ $command = new AcknowledgeProblemCommand();
+ $command->setObjects($granted);
+ $command->setComment($this->getValue('comment'));
+ $command->setAuthor($this->getAuth()->getUser()->getUsername());
+ $command->setNotify($this->getElement('notify')->isChecked());
+ $command->setSticky($this->getElement('sticky')->isChecked());
+ $command->setPersistent($this->getElement('persistent')->isChecked());
+
+ if (($expireTime = $this->getValue('expire_time')) !== null) {
+ /** @var DateTime $expireTime */
+ $command->setExpireTime($expireTime->getTimestamp());
+ }
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/AddCommentForm.php b/application/forms/Command/Object/AddCommentForm.php
new file mode 100644
index 0000000..9cd0754
--- /dev/null
+++ b/application/forms/Command/Object/AddCommentForm.php
@@ -0,0 +1,162 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use DateInterval;
+use DateTime;
+use Icinga\Application\Config;
+use Icinga\Module\Icingadb\Command\Object\AddCommentCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Web\Notification;
+use ipl\Html\Attributes;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Validator\CallbackValidator;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use ipl\Web\Widget\Icon;
+use Traversable;
+
+use function ipl\Stdlib\iterable_value_first;
+
+class AddCommentForm extends CommandForm
+{
+ public function __construct()
+ {
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ $countObjects = count($this->getObjects());
+ if (iterable_value_first($this->getObjects()) instanceof Host) {
+ $message = sprintf(
+ tp('Added comment successfully', 'Added comment to %d hosts successfully', $countObjects),
+ $countObjects
+ );
+ } else {
+ $message = sprintf(
+ tp('Added comment successfully', 'Added comment to %d services successfully', $countObjects),
+ $countObjects
+ );
+ }
+
+ Notification::success($message);
+ });
+ }
+
+ protected function assembleElements()
+ {
+ $this->addHtml(new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'form-description']),
+ new Icon('info-circle', ['class' => 'form-description-icon']),
+ new HtmlElement(
+ 'ul',
+ null,
+ new HtmlElement(
+ 'li',
+ null,
+ Text::create(t('This command is used to add host or service comments.'))
+ )
+ )
+ ));
+
+ $decorator = new IcingaFormDecorator();
+
+ $this->addElement(
+ 'textarea',
+ 'comment',
+ [
+ 'required' => true,
+ 'label' => t('Comment'),
+ 'description' => t(
+ 'If you work with other administrators, you may find it useful to share information about'
+ . ' the host or service that is having problems. Make sure you enter a brief description of'
+ . ' what you are doing.'
+ )
+ ]
+ );
+ $decorator->decorate($this->getElement('comment'));
+
+ $config = Config::module('icingadb');
+
+ $this->addElement(
+ 'checkbox',
+ 'expire',
+ [
+ 'ignore' => true,
+ 'class' => 'autosubmit',
+ 'value' => (bool) $config->get('settings', 'comment_expire', false),
+ 'label' => t('Use Expire Time'),
+ 'description' => t('If the comment should expire, check this option.')
+ ]
+ );
+ $decorator->decorate($this->getElement('expire'));
+
+ if ($this->getElement('expire')->isChecked()) {
+ $expireTime = new DateTime();
+ $expireTime->add(new DateInterval($config->get('settings', 'comment_expire_time', 'PT1H')));
+
+ $this->addElement(
+ 'localDateTime',
+ 'expire_time',
+ [
+ 'data-use-datetime-picker' => true,
+ 'required' => true,
+ 'value' => $expireTime,
+ 'label' => t('Expire Time'),
+ 'description' => t('Choose the date and time when Icinga should delete the comment.'),
+ 'validators' => [
+ 'DateTime' => ['break_chain_on_failure' => true],
+ 'Callback' => function ($value, $validator) {
+ /** @var CallbackValidator $validator */
+ if ($value <= (new DateTime())) {
+ $validator->addMessage(t('The expire time must not be in the past'));
+ return false;
+ }
+
+ return true;
+ }
+ ]
+ ]
+ );
+ $decorator->decorate($this->getElement('expire_time'));
+ }
+ }
+
+ protected function assembleSubmitButton()
+ {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ [
+ 'required' => true,
+ 'label' => tp('Add comment', 'Add comments', count($this->getObjects()))
+ ]
+ );
+
+ (new IcingaFormDecorator())->decorate($this->getElement('btn_submit'));
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ $granted = $this->filterGrantedOn('icingadb/command/comment/add', $objects);
+
+ if ($granted->valid()) {
+ $command = new AddCommentCommand();
+ $command->setObjects($granted);
+ $command->setComment($this->getValue('comment'));
+ $command->setAuthor($this->getAuth()->getUser()->getUsername());
+
+ if (($expireTime = $this->getValue('expire_time'))) {
+ /** @var DateTime $expireTime */
+ $command->setExpireTime($expireTime->getTimestamp());
+ }
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/CheckNowForm.php b/application/forms/Command/Object/CheckNowForm.php
new file mode 100644
index 0000000..b7a506c
--- /dev/null
+++ b/application/forms/Command/Object/CheckNowForm.php
@@ -0,0 +1,72 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use Generator;
+use Icinga\Module\Icingadb\Command\Object\ScheduleCheckCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+use ipl\Web\Widget\Icon;
+use Traversable;
+
+class CheckNowForm extends CommandForm
+{
+ protected $defaultAttributes = ['class' => 'inline'];
+
+ public function __construct()
+ {
+ $this->on(self::ON_SUCCESS, function () {
+ if (! $this->errorOccurred) {
+ Notification::success(tp('Scheduling check..', 'Scheduling checks..', count($this->getObjects())));
+ }
+ });
+ }
+
+ protected function assembleElements()
+ {
+ }
+
+ protected function assembleSubmitButton()
+ {
+ $this->addElement(
+ 'submitButton',
+ 'btn_submit',
+ [
+ 'class' => ['link-button', 'spinner'],
+ 'label' => [
+ new Icon('sync-alt'),
+ t('Check Now')
+ ],
+ 'title' => t('Schedule the next active check to run immediately')
+ ]
+ );
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ $granted = (function () use ($objects): Generator {
+ foreach ($objects as $object) {
+ if (
+ $this->isGrantedOn('icingadb/command/schedule-check', $object)
+ || (
+ $object->active_checks_enabled
+ && $this->isGrantedOn('icingadb/command/schedule-check/active-only', $object)
+ )
+ ) {
+ yield $object;
+ }
+ }
+ })();
+
+ if ($granted->valid()) {
+ $command = new ScheduleCheckCommand();
+ $command->setObjects($granted);
+ $command->setCheckTime(time());
+ $command->setForced();
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/DeleteCommentForm.php b/application/forms/Command/Object/DeleteCommentForm.php
new file mode 100644
index 0000000..25275ba
--- /dev/null
+++ b/application/forms/Command/Object/DeleteCommentForm.php
@@ -0,0 +1,75 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use Generator;
+use Icinga\Module\Icingadb\Command\Object\DeleteCommentCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+use ipl\Web\Common\RedirectOption;
+use ipl\Web\Widget\Icon;
+use Traversable;
+
+class DeleteCommentForm extends CommandForm
+{
+ use RedirectOption;
+
+ protected $defaultAttributes = ['class' => 'inline'];
+
+ public function __construct()
+ {
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ $countObjects = count($this->getObjects());
+
+ Notification::success(sprintf(
+ tp('Removed comment successfully', 'Removed comment from %d objects successfully', $countObjects),
+ $countObjects
+ ));
+ });
+ }
+
+ protected function assembleElements()
+ {
+ $this->addElement($this->createRedirectOption());
+ }
+
+ protected function assembleSubmitButton()
+ {
+ $this->addElement(
+ 'submitButton',
+ 'btn_submit',
+ [
+ 'class' => ['cancel-button', 'spinner'],
+ 'label' => [
+ new Icon('trash'),
+ tp('Remove Comment', 'Remove Comments', count($this->getObjects()))
+ ]
+ ]
+ );
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ $granted = (function () use ($objects): Generator {
+ foreach ($objects as $object) {
+ if ($this->isGrantedOn('icingadb/command/comment/delete', $object->{$object->object_type})) {
+ yield $object;
+ }
+ }
+ })();
+
+ if ($granted->valid()) {
+ $command = new DeleteCommentCommand();
+ $command->setObjects($granted);
+ $command->setAuthor($this->getAuth()->getUser()->getUsername());
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/DeleteDowntimeForm.php b/application/forms/Command/Object/DeleteDowntimeForm.php
new file mode 100644
index 0000000..5f695b9
--- /dev/null
+++ b/application/forms/Command/Object/DeleteDowntimeForm.php
@@ -0,0 +1,90 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use Generator;
+use Icinga\Module\Icingadb\Command\Object\DeleteDowntimeCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+use ipl\Web\Common\RedirectOption;
+use ipl\Web\Widget\Icon;
+use Traversable;
+
+class DeleteDowntimeForm extends CommandForm
+{
+ use RedirectOption;
+
+ protected $defaultAttributes = ['class' => 'inline'];
+
+ public function __construct()
+ {
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ $countObjects = count($this->getObjects());
+
+ Notification::success(sprintf(
+ tp('Removed downtime successfully', 'Removed downtime from %d objects successfully', $countObjects),
+ $countObjects
+ ));
+ });
+ }
+
+ protected function assembleElements()
+ {
+ $this->addElement($this->createRedirectOption());
+ }
+
+ protected function assembleSubmitButton()
+ {
+ $isDisabled = true;
+ foreach ($this->getObjects() as $downtime) {
+ if ($downtime->scheduled_by === null) {
+ $isDisabled = false;
+ break;
+ }
+ }
+
+ $this->addElement(
+ 'submitButton',
+ 'btn_submit',
+ [
+ 'class' => ['cancel-button', 'spinner'],
+ 'disabled' => $isDisabled ?: null,
+ 'title' => $isDisabled
+ ? t('Downtime cannot be removed at runtime because it is based on a configured scheduled downtime.')
+ : null,
+ 'label' => [
+ new Icon('trash'),
+ tp('Delete downtime', 'Delete downtimes', count($this->getObjects()))
+ ]
+ ]
+ );
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ $granted = (function () use ($objects): Generator {
+ foreach ($objects as $object) {
+ if (
+ $this->isGrantedOn('icingadb/command/downtime/delete', $object->{$object->object_type})
+ && $object->scheduled_by === null
+ ) {
+ yield $object;
+ }
+ }
+ })();
+
+ if ($granted->valid()) {
+ $command = new DeleteDowntimeCommand();
+ $command->setObjects($granted);
+ $command->setAuthor($this->getAuth()->getUser()->getUsername());
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/ProcessCheckResultForm.php b/application/forms/Command/Object/ProcessCheckResultForm.php
new file mode 100644
index 0000000..5764bf8
--- /dev/null
+++ b/application/forms/Command/Object/ProcessCheckResultForm.php
@@ -0,0 +1,156 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use Generator;
+use Icinga\Module\Icingadb\Command\Object\ProcessCheckResultCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Web\Notification;
+use ipl\Html\Attributes;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Orm\Model;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use ipl\Web\Widget\Icon;
+use Traversable;
+
+use function ipl\Stdlib\iterable_value_first;
+
+class ProcessCheckResultForm extends CommandForm
+{
+ public function __construct()
+ {
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ $countObjects = count($this->getObjects());
+ if (iterable_value_first($this->getObjects()) instanceof Host) {
+ $message = sprintf(tp(
+ 'Submitted passive check result successfully',
+ 'Submitted passive check result for %d hosts successfully',
+ $countObjects
+ ), $countObjects);
+ } else {
+ $message = sprintf(tp(
+ 'Submitted passive check result successfully',
+ 'Submitted passive check result for %d services successfully',
+ $countObjects
+ ), $countObjects);
+ }
+
+ Notification::success($message);
+ });
+ }
+
+ protected function assembleElements()
+ {
+ $this->addHtml(new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'form-description']),
+ new Icon('info-circle', ['class' => 'form-description-icon']),
+ new HtmlElement(
+ 'ul',
+ null,
+ new HtmlElement(
+ 'li',
+ null,
+ Text::create(t('This command is used to submit passive host or service check results.'))
+ )
+ )
+ ));
+
+ $decorator = new IcingaFormDecorator();
+
+ /** @var Model $object */
+ $object = iterable_value_first($this->getObjects());
+
+ $this->addElement(
+ 'select',
+ 'status',
+ [
+ 'required' => true,
+ 'label' => t('Status'),
+ 'description' => t('The state this check result should report'),
+ 'options' => $object instanceof Host ? [
+ ProcessCheckResultCommand::HOST_UP => t('UP', 'icinga.state'),
+ ProcessCheckResultCommand::HOST_DOWN => t('DOWN', 'icinga.state')
+ ] : [
+ ProcessCheckResultCommand::SERVICE_OK => t('OK', 'icinga.state'),
+ ProcessCheckResultCommand::SERVICE_WARNING => t('WARNING', 'icinga.state'),
+ ProcessCheckResultCommand::SERVICE_CRITICAL => t('CRITICAL', 'icinga.state'),
+ ProcessCheckResultCommand::SERVICE_UNKNOWN => t('UNKNOWN', 'icinga.state')
+ ]
+ ]
+ );
+ $decorator->decorate($this->getElement('status'));
+
+ $this->addElement(
+ 'text',
+ 'output',
+ [
+ 'required' => true,
+ 'label' => t('Output'),
+ 'description' => t('The plugin output of this check result')
+ ]
+ );
+ $decorator->decorate($this->getElement('output'));
+
+ $this->addElement(
+ 'text',
+ 'perfdata',
+ [
+ 'allowEmpty' => true,
+ 'label' => t('Performance Data'),
+ 'description' => t(
+ 'The performance data of this check result. Leave empty'
+ . ' if this check result has no performance data'
+ )
+ ]
+ );
+ $decorator->decorate($this->getElement('perfdata'));
+ }
+
+ protected function assembleSubmitButton()
+ {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ [
+ 'required' => true,
+ 'label' => tp(
+ 'Submit Passive Check Result',
+ 'Submit Passive Check Results',
+ count($this->getObjects())
+ )
+ ]
+ );
+
+ (new IcingaFormDecorator())->decorate($this->getElement('btn_submit'));
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ $granted = (function () use ($objects): Generator {
+ foreach ($this->filterGrantedOn('icingadb/command/process-check-result', $objects) as $object) {
+ if ($object->passive_checks_enabled) {
+ yield $object;
+ }
+ }
+ })();
+
+ if ($granted->valid()) {
+ $command = new ProcessCheckResultCommand();
+ $command->setObjects($granted);
+ $command->setStatus($this->getValue('status'));
+ $command->setOutput($this->getValue('output'));
+ $command->setPerformanceData($this->getValue('perfdata'));
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/RemoveAcknowledgementForm.php b/application/forms/Command/Object/RemoveAcknowledgementForm.php
new file mode 100644
index 0000000..8697985
--- /dev/null
+++ b/application/forms/Command/Object/RemoveAcknowledgementForm.php
@@ -0,0 +1,77 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use Icinga\Module\Icingadb\Command\Object\RemoveAcknowledgementCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Web\Notification;
+use ipl\Web\Widget\Icon;
+use Traversable;
+
+use function ipl\Stdlib\iterable_value_first;
+
+class RemoveAcknowledgementForm extends CommandForm
+{
+ public function __construct()
+ {
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ $countObjects = count($this->getObjects());
+ if (iterable_value_first($this->getObjects()) instanceof Host) {
+ $message = sprintf(tp(
+ 'Removed acknowledgment successfully',
+ 'Removed acknowledgment from %d hosts successfully',
+ $countObjects
+ ), $countObjects);
+ } else {
+ $message = sprintf(tp(
+ 'Removed acknowledgment successfully',
+ 'Removed acknowledgment from %d services successfully',
+ $countObjects
+ ), $countObjects);
+ }
+
+ Notification::success($message);
+ });
+ }
+
+ protected $defaultAttributes = ['class' => 'inline'];
+
+ protected function assembleElements()
+ {
+ }
+
+ protected function assembleSubmitButton()
+ {
+ $this->addElement(
+ 'submitButton',
+ 'btn_submit',
+ [
+ 'class' => ['link-button', 'spinner'],
+ 'label' => [
+ new Icon('trash'),
+ tp('Remove acknowledgement', 'Remove acknowledgements', count($this->getObjects()))
+ ]
+ ]
+ );
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ $granted = $this->filterGrantedOn('icingadb/command/remove-acknowledgement', $objects);
+
+ if ($granted->valid()) {
+ $command = new RemoveAcknowledgementCommand();
+ $command->setObjects($granted);
+ $command->setAuthor($this->getAuth()->getUser()->getUsername());
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/ScheduleCheckForm.php b/application/forms/Command/Object/ScheduleCheckForm.php
new file mode 100644
index 0000000..9b32ea1
--- /dev/null
+++ b/application/forms/Command/Object/ScheduleCheckForm.php
@@ -0,0 +1,137 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use DateInterval;
+use DateTime;
+use Generator;
+use Icinga\Module\Icingadb\Command\Object\ScheduleCheckCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Web\Notification;
+use ipl\Html\Attributes;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use ipl\Web\Widget\Icon;
+use Traversable;
+
+use function ipl\Stdlib\iterable_value_first;
+
+class ScheduleCheckForm extends CommandForm
+{
+ public function __construct()
+ {
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ $countObjects = count($this->getObjects());
+ if (iterable_value_first($this->getObjects()) instanceof Host) {
+ $message = sprintf(
+ tp('Scheduled check successfully', 'Scheduled check for %d hosts successfully', $countObjects),
+ $countObjects
+ );
+ } else {
+ $message = sprintf(
+ tp('Scheduled check successfully', 'Scheduled check for %d services successfully', $countObjects),
+ $countObjects
+ );
+ }
+
+ Notification::success($message);
+ });
+ }
+
+ protected function assembleElements()
+ {
+ $this->addHtml(new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'form-description']),
+ new Icon('info-circle', ['class' => 'form-description-icon']),
+ new HtmlElement(
+ 'ul',
+ null,
+ new HtmlElement(
+ 'li',
+ null,
+ Text::create(t(
+ 'This command is used to schedule the next check of hosts or services. Icinga'
+ . ' will re-queue the hosts or services to be checked at the time you specify.'
+ ))
+ )
+ )
+ ));
+
+ $decorator = new IcingaFormDecorator();
+
+ $this->addElement(
+ 'localDateTime',
+ 'check_time',
+ [
+ 'data-use-datetime-picker' => true,
+ 'required' => true,
+ 'label' => t('Check Time'),
+ 'description' => t('Set the date and time when the check should be scheduled.'),
+ 'value' => (new DateTime())->add(new DateInterval('PT1H'))
+ ]
+ );
+ $decorator->decorate($this->getElement('check_time'));
+
+ $this->addElement(
+ 'checkbox',
+ 'force_check',
+ [
+ 'label' => t('Force Check'),
+ 'description' => t(
+ 'If you select this option, Icinga will force a check regardless of both what time the'
+ . ' scheduled check occurs and whether or not checks are enabled.'
+ )
+ ]
+ );
+ $decorator->decorate($this->getElement('force_check'));
+ }
+
+ protected function assembleSubmitButton()
+ {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ [
+ 'required' => true,
+ 'label' => tp('Schedule check', 'Schedule checks', count($this->getObjects()))
+ ]
+ );
+
+ (new IcingaFormDecorator())->decorate($this->getElement('btn_submit'));
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ $granted = (function () use ($objects): Generator {
+ foreach ($objects as $object) {
+ if (
+ $this->isGrantedOn('icingadb/command/schedule-check', $object)
+ || (
+ $object->active_checks_enabled
+ && $this->isGrantedOn('icingadb/command/schedule-check/active-only', $object)
+ )
+ ) {
+ yield $object;
+ }
+ }
+ })();
+
+ if ($granted->valid()) {
+ $command = new ScheduleCheckCommand();
+ $command->setObjects($granted);
+ $command->setForced($this->getElement('force_check')->isChecked());
+ $command->setCheckTime($this->getValue('check_time')->getTimestamp());
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/ScheduleHostDowntimeForm.php b/application/forms/Command/Object/ScheduleHostDowntimeForm.php
new file mode 100644
index 0000000..bc21114
--- /dev/null
+++ b/application/forms/Command/Object/ScheduleHostDowntimeForm.php
@@ -0,0 +1,119 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use DateInterval;
+use DateTime;
+use Icinga\Application\Config;
+use Icinga\Module\Icingadb\Command\Object\PropagateHostDowntimeCommand;
+use Icinga\Module\Icingadb\Command\Object\ScheduleHostDowntimeCommand;
+use Icinga\Web\Notification;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use Traversable;
+
+class ScheduleHostDowntimeForm extends ScheduleServiceDowntimeForm
+{
+ /** @var bool */
+ protected $hostDowntimeAllServices;
+
+ public function __construct()
+ {
+ $this->start = new DateTime();
+ $config = Config::module('icingadb');
+ $this->commentText = $config->get('settings', 'hostdowntime_comment_text');
+
+ $this->hostDowntimeAllServices = (bool) $config->get('settings', 'hostdowntime_all_services', false);
+
+ $fixedEnd = clone $this->start;
+ $fixed = $config->get('settings', 'hostdowntime_end_fixed', 'PT1H');
+ $this->fixedEnd = $fixedEnd->add(new DateInterval($fixed));
+
+ $flexibleEnd = clone $this->start;
+ $flexible = $config->get('settings', 'hostdowntime_end_flexible', 'PT2H');
+ $this->flexibleEnd = $flexibleEnd->add(new DateInterval($flexible));
+
+ $flexibleDuration = $config->get('settings', 'hostdowntime_flexible_duration', 'PT2H');
+ $this->flexibleDuration = new DateInterval($flexibleDuration);
+
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ $countObjects = count($this->getObjects());
+
+ Notification::success(sprintf(
+ tp('Scheduled downtime successfully', 'Scheduled downtime for %d hosts successfully', $countObjects),
+ $countObjects
+ ));
+ });
+ }
+
+ protected function assembleElements()
+ {
+ parent::assembleElements();
+
+ $decorator = new IcingaFormDecorator();
+
+ $this->addElement(
+ 'checkbox',
+ 'all_services',
+ [
+ 'label' => t('All Services'),
+ 'description' => t(
+ 'Sets downtime for all services for the matched host objects. If child options are set,'
+ . ' all child hosts and their services will schedule a downtime too.'
+ ),
+ 'value' => $this->hostDowntimeAllServices
+ ]
+ );
+ $decorator->decorate($this->getElement('all_services'));
+
+ $this->addElement(
+ 'select',
+ 'child_options',
+ array(
+ 'description' => t('Schedule child downtimes.'),
+ 'label' => t('Child Options'),
+ 'options' => [
+ 0 => t('Do nothing with child hosts'),
+ 1 => t('Schedule triggered downtime for all child hosts'),
+ 2 => t('Schedule non-triggered downtime for all child hosts')
+ ]
+ )
+ );
+ $decorator->decorate($this->getElement('child_options'));
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ $granted = $this->filterGrantedOn('icingadb/command/downtime/schedule', $objects);
+
+ if ($granted->valid()) {
+ if (($childOptions = (int) $this->getValue('child_options'))) {
+ $command = new PropagateHostDowntimeCommand();
+ $command->setTriggered($childOptions === 1);
+ } else {
+ $command = new ScheduleHostDowntimeCommand();
+ }
+
+ $command->setObjects($granted);
+ $command->setComment($this->getValue('comment'));
+ $command->setAuthor($this->getAuth()->getUser()->getUsername());
+ $command->setStart($this->getValue('start')->getTimestamp());
+ $command->setEnd($this->getValue('end')->getTimestamp());
+ $command->setForAllServices($this->getElement('all_services')->isChecked());
+
+ if ($this->getElement('flexible')->isChecked()) {
+ $command->setFixed(false);
+ $command->setDuration(
+ $this->getValue('hours') * 3600 + $this->getValue('minutes') * 60
+ );
+ }
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/ScheduleServiceDowntimeForm.php b/application/forms/Command/Object/ScheduleServiceDowntimeForm.php
new file mode 100644
index 0000000..184a4e8
--- /dev/null
+++ b/application/forms/Command/Object/ScheduleServiceDowntimeForm.php
@@ -0,0 +1,267 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use DateInterval;
+use DateTime;
+use Icinga\Application\Config;
+use Icinga\Module\Icingadb\Command\Object\ScheduleServiceDowntimeCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+use ipl\Html\Attributes;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Validator\CallbackValidator;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use ipl\Web\Widget\Icon;
+use Traversable;
+
+class ScheduleServiceDowntimeForm extends CommandForm
+{
+ /** @var DateTime downtime start */
+ protected $start;
+
+ /** @var DateTime fixed downtime end */
+ protected $fixedEnd;
+
+ /**@var DateTime flexible downtime end */
+ protected $flexibleEnd;
+
+ /** @var DateInterval flexible downtime duration */
+ protected $flexibleDuration;
+
+ /** @var mixed Comment Text */
+ protected $commentText;
+
+ /**
+ * Initialize this form
+ */
+ public function __construct()
+ {
+ $this->start = new DateTime();
+
+ $config = Config::module('icingadb');
+
+ $this->commentText = $config->get('settings', 'hostdowntime_comment_text');
+ $fixedEnd = clone $this->start;
+ $fixed = $config->get('settings', 'servicedowntime_end_fixed', 'PT1H');
+ $this->fixedEnd = $fixedEnd->add(new DateInterval($fixed));
+
+ $flexibleEnd = clone $this->start;
+ $flexible = $config->get('settings', 'servicedowntime_end_flexible', 'PT2H');
+ $this->flexibleEnd = $flexibleEnd->add(new DateInterval($flexible));
+
+ $flexibleDuration = $config->get('settings', 'servicedowntime_flexible_duration', 'PT2H');
+ $this->flexibleDuration = new DateInterval($flexibleDuration);
+
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ $countObjects = count($this->getObjects());
+
+ Notification::success(sprintf(
+ tp('Scheduled downtime successfully', 'Scheduled downtime for %d services successfully', $countObjects),
+ $countObjects
+ ));
+ });
+ }
+
+ protected function assembleElements()
+ {
+ $isFlexible = $this->getPopulatedValue('flexible') === 'y';
+
+ $this->addHtml(new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'form-description']),
+ new Icon('info-circle', ['class' => 'form-description-icon']),
+ new HtmlElement(
+ 'ul',
+ null,
+ new HtmlElement(
+ 'li',
+ null,
+ Text::create(t(
+ 'This command is used to schedule host and service downtimes. During the downtime specified'
+ . ' by the start and end time, Icinga will not send notifications out about the hosts and'
+ . ' services. When the scheduled downtime expires, Icinga will send out notifications for'
+ . ' the hosts and services as it normally would.'
+ ))
+ )
+ )
+ ));
+
+ $decorator = new IcingaFormDecorator();
+
+ $this->addElement(
+ 'textarea',
+ 'comment',
+ [
+ 'required' => true,
+ 'label' => t('Comment'),
+ 'description' => t(
+ 'If you work with other administrators, you may find it useful to share information about'
+ . ' the host or service that is having problems. Make sure you enter a brief description of'
+ . ' what you are doing.'
+ ),
+ 'value' => $this->commentText
+ ]
+ );
+ $decorator->decorate($this->getElement('comment'));
+
+ $this->addElement(
+ 'localDateTime',
+ 'start',
+ [
+ 'data-use-datetime-picker' => true,
+ 'required' => true,
+ 'value' => $this->start,
+ 'label' => t('Start Time'),
+ 'description' => t('Set the start date and time for the downtime.')
+ ]
+ );
+ $decorator->decorate($this->getElement('start'));
+
+ $this->addElement(
+ 'localDateTime',
+ 'end',
+ [
+ 'data-use-datetime-picker' => true,
+ 'required' => true,
+ 'label' => t('End Time'),
+ 'description' => t('Set the end date and time for the downtime.'),
+ 'value' => $isFlexible ? $this->flexibleEnd : $this->fixedEnd,
+ 'validators' => [
+ 'DateTime' => ['break_chain_on_failure' => true],
+ 'Callback' => function ($value, $validator) {
+ /** @var CallbackValidator $validator */
+
+ if ($value <= $this->getValue('start')) {
+ $validator->addMessage(t('The end time must be greater than the start time'));
+ return false;
+ }
+
+ if ($value <= (new DateTime())) {
+ $validator->addMessage(t('A downtime must not be in the past'));
+ return false;
+ }
+
+ return true;
+ }
+ ]
+ ]
+ );
+ $decorator->decorate($this->getElement('end'));
+
+ $this->addElement(
+ 'checkbox',
+ 'flexible',
+ [
+ 'class' => 'autosubmit',
+ 'label' => t('Flexible'),
+ 'description' => t(
+ 'To make this a flexible downtime, check this option. A flexible downtime starts when the host'
+ . ' or service enters a problem state sometime between the start and end times you specified.'
+ . ' It then lasts as long as the duration time you enter.'
+ )
+ ]
+ );
+ $decorator->decorate($this->getElement('flexible'));
+
+ if ($isFlexible) {
+ $hoursInput = $this->createElement(
+ 'number',
+ 'hours',
+ [
+ 'required' => true,
+ 'label' => t('Duration'),
+ 'value' => $this->flexibleDuration->h,
+ 'min' => 0,
+ 'validators' => [
+ 'Callback' => function ($value, $validator) {
+ /** @var CallbackValidator $validator */
+
+ if ($this->getValue('minutes') == 0 && $value == 0) {
+ $validator->addMessage(t('The duration must not be zero'));
+ return false;
+ }
+
+ return true;
+ }
+ ]
+ ]
+ );
+ $this->registerElement($hoursInput);
+ $decorator->decorate($hoursInput);
+
+ $minutesInput = $this->createElement(
+ 'number',
+ 'minutes',
+ [
+ 'required' => true,
+ 'value' => $this->flexibleDuration->m,
+ 'min' => 0
+ ]
+ );
+ $this->registerElement($minutesInput);
+ $minutesInput->addWrapper(
+ new HtmlElement('label', null, new HtmlElement('span', null, Text::create(t('Minutes'))))
+ );
+
+ $hoursInput->getWrapper()->on(
+ IcingaFormDecorator::ON_ASSEMBLED,
+ function ($hoursInputWrapper) use ($minutesInput, $hoursInput) {
+ $hoursInputWrapper
+ ->insertAfter($minutesInput, $hoursInput)
+ ->getAttributes()->add('class', 'downtime-duration');
+ }
+ );
+
+ $hoursInput->prependWrapper(
+ new HtmlElement('label', null, new HtmlElement('span', null, Text::create(t('Hours'))))
+ );
+
+ $this->add($hoursInput);
+ }
+ }
+
+ protected function assembleSubmitButton()
+ {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ [
+ 'required' => true,
+ 'label' => tp('Schedule downtime', 'Schedule downtimes', count($this->getObjects()))
+ ]
+ );
+
+ (new IcingaFormDecorator())->decorate($this->getElement('btn_submit'));
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ $granted = $this->filterGrantedOn('icingadb/command/downtime/schedule', $objects);
+
+ if ($granted->valid()) {
+ $command = new ScheduleServiceDowntimeCommand();
+ $command->setObjects($granted);
+ $command->setComment($this->getValue('comment'));
+ $command->setAuthor($this->getAuth()->getUser()->getUsername());
+ $command->setStart($this->getValue('start')->getTimestamp());
+ $command->setEnd($this->getValue('end')->getTimestamp());
+
+ if ($this->getElement('flexible')->isChecked()) {
+ $command->setFixed(false);
+ $command->setDuration(
+ $this->getValue('hours') * 3600 + $this->getValue('minutes') * 60
+ );
+ }
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/SendCustomNotificationForm.php b/application/forms/Command/Object/SendCustomNotificationForm.php
new file mode 100644
index 0000000..dfb1e96
--- /dev/null
+++ b/application/forms/Command/Object/SendCustomNotificationForm.php
@@ -0,0 +1,125 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use Icinga\Application\Config;
+use Icinga\Module\Icingadb\Command\Object\SendCustomNotificationCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Web\Notification;
+use ipl\Html\Attributes;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use ipl\Web\Widget\Icon;
+use Traversable;
+
+use function ipl\Stdlib\iterable_value_first;
+
+class SendCustomNotificationForm extends CommandForm
+{
+ public function __construct()
+ {
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ $countObjects = count($this->getObjects());
+ if (iterable_value_first($this->getObjects()) instanceof Host) {
+ $message = sprintf(tp(
+ 'Sent custom notification successfully',
+ 'Sent custom notification for %d hosts successfully',
+ $countObjects
+ ), $countObjects);
+ } else {
+ $message = sprintf(tp(
+ 'Sent custom notification successfully',
+ 'Sent custom notification for %d services successfully',
+ $countObjects
+ ), $countObjects);
+ }
+
+ Notification::success($message);
+ });
+ }
+
+ protected function assembleElements()
+ {
+ $this->addHtml(new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'form-description']),
+ new Icon('info-circle', ['class' => 'form-description-icon']),
+ new HtmlElement(
+ 'ul',
+ null,
+ new HtmlElement(
+ 'li',
+ null,
+ Text::create(t('This command is used to send custom notifications about hosts or services.'))
+ )
+ )
+ ));
+
+ $config = Config::module('icingadb');
+ $decorator = new IcingaFormDecorator();
+
+ $this->addElement(
+ 'textarea',
+ 'comment',
+ [
+ 'required' => true,
+ 'label' => t('Comment'),
+ 'description' => t(
+ 'Enter a brief description on why you\'re sending this notification. It will be sent with it.'
+ )
+ ]
+ );
+ $decorator->decorate($this->getElement('comment'));
+
+ $this->addElement(
+ 'checkbox',
+ 'forced',
+ [
+ 'label' => t('Forced'),
+ 'value' => (bool) $config->get('settings', 'custom_notification_forced', false),
+ 'description' => t(
+ 'If you check this option, the notification is sent regardless'
+ . ' of downtimes or whether notifications are enabled or not.'
+ )
+ ]
+ );
+ $decorator->decorate($this->getElement('forced'));
+ }
+
+ protected function assembleSubmitButton()
+ {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ [
+ 'required' => true,
+ 'label' => tp('Send custom notification', 'Send custom notifications', count($this->getObjects()))
+ ]
+ );
+
+ (new IcingaFormDecorator())->decorate($this->getElement('btn_submit'));
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ $granted = $this->filterGrantedOn('icingadb/command/send-custom-notification', $objects);
+
+ if ($granted->valid()) {
+ $command = new SendCustomNotificationCommand();
+ $command->setObjects($granted);
+ $command->setComment($this->getValue('comment'));
+ $command->setForced($this->getElement('forced')->isChecked());
+ $command->setAuthor($this->getAuth()->getUser()->getUsername());
+
+ yield $command;
+ }
+ }
+}
diff --git a/application/forms/Command/Object/ToggleObjectFeaturesForm.php b/application/forms/Command/Object/ToggleObjectFeaturesForm.php
new file mode 100644
index 0000000..50767da
--- /dev/null
+++ b/application/forms/Command/Object/ToggleObjectFeaturesForm.php
@@ -0,0 +1,186 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Command\Object;
+
+use Icinga\Module\Icingadb\Command\Object\ToggleObjectFeatureCommand;
+use Icinga\Module\Icingadb\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+use ipl\Html\FormElement\CheckboxElement;
+use ipl\Orm\Model;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use Traversable;
+
+class ToggleObjectFeaturesForm extends CommandForm
+{
+ const LEAVE_UNCHANGED = 'noop';
+
+ protected $features;
+
+ protected $featureStatus;
+
+ /**
+ * ToggleFeature(s) being used to submit this form
+ *
+ * @var ToggleObjectFeatureCommand[]
+ */
+ protected $submittedFeatures = [];
+
+ public function __construct($featureStatus)
+ {
+ $this->featureStatus = $featureStatus;
+ $this->features = [
+ ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS => [
+ 'label' => t('Active Checks'),
+ 'permission' => 'icingadb/command/feature/object/active-checks'
+ ],
+ ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS => [
+ 'label' => t('Passive Checks'),
+ 'permission' => 'icingadb/command/feature/object/passive-checks'
+ ],
+ ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS => [
+ 'label' => t('Notifications'),
+ 'permission' => 'icingadb/command/feature/object/notifications'
+ ],
+ ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER => [
+ 'label' => t('Event Handler'),
+ 'permission' => 'icingadb/command/feature/object/event-handler'
+ ],
+ ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION => [
+ 'label' => t('Flap Detection'),
+ 'permission' => 'icingadb/command/feature/object/flap-detection'
+ ]
+ ];
+
+ $this->getAttributes()->add('class', 'object-features');
+
+ $this->on(self::ON_SUCCESS, function () {
+ if ($this->errorOccurred) {
+ return;
+ }
+
+ foreach ($this->submittedFeatures as $feature) {
+ $enabled = $feature->getEnabled();
+ switch ($feature->getFeature()) {
+ case ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS:
+ if ($enabled) {
+ $message = t('Enabled active checks successfully');
+ } else {
+ $message = t('Disabled active checks successfully');
+ }
+
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS:
+ if ($enabled) {
+ $message = t('Enabled passive checks successfully');
+ } else {
+ $message = t('Disabled passive checks successfully');
+ }
+
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER:
+ if ($enabled) {
+ $message = t('Enabled event handler successfully');
+ } else {
+ $message = t('Disabled event handler checks successfully');
+ }
+
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION:
+ if ($enabled) {
+ $message = t('Enabled flap detection successfully');
+ } else {
+ $message = t('Disabled flap detection successfully');
+ }
+
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS:
+ if ($enabled) {
+ $message = t('Enabled notifications successfully');
+ } else {
+ $message = t('Disabled notifications successfully');
+ }
+
+ break;
+ default:
+ $message = t('Invalid feature option');
+ break;
+ }
+
+ Notification::success($message);
+ }
+ });
+ }
+
+ protected function assembleElements()
+ {
+ $decorator = new IcingaFormDecorator();
+ foreach ($this->features as $feature => $spec) {
+ $options = [
+ 'class' => 'autosubmit',
+ 'disabled' => $this->featureStatus instanceof Model
+ ? ! $this->isGrantedOn($spec['permission'], $this->featureStatus)
+ : false,
+ 'label' => $spec['label']
+ ];
+ if ($this->featureStatus[$feature] === 2) {
+ $this->addElement(
+ 'select',
+ $feature,
+ $options + [
+ 'description' => t('Multiple Values'),
+ 'options' => [
+ self::LEAVE_UNCHANGED => t('Leave Unchanged'),
+ t('Disable All'),
+ t('Enable All')
+ ],
+ 'value' => self::LEAVE_UNCHANGED
+ ]
+ );
+ $decorator->decorate($this->getElement($feature));
+
+ $this->getElement($feature)
+ ->getWrapper()
+ ->getAttributes()
+ ->add('class', 'indeterminate');
+ } else {
+ $options['value'] = (bool) $this->featureStatus[$feature];
+ $this->addElement('checkbox', $feature, $options);
+ $decorator->decorate($this->getElement($feature));
+ }
+ }
+ }
+
+ protected function assembleSubmitButton()
+ {
+ }
+
+ protected function getCommands(Traversable $objects): Traversable
+ {
+ foreach ($this->features as $feature => $spec) {
+ if ($this->getElement($feature) instanceof CheckboxElement) {
+ $state = $this->getElement($feature)->isChecked();
+ } else {
+ $state = $this->getElement($feature)->getValue();
+ }
+
+ if ($state === self::LEAVE_UNCHANGED || (int) $state === (int) $this->featureStatus[$feature]) {
+ continue;
+ }
+
+ $granted = $this->filterGrantedOn($spec['permission'], $objects);
+
+ if ($granted->valid()) {
+ $command = new ToggleObjectFeatureCommand();
+ $command->setObjects($granted);
+ $command->setFeature($feature);
+ $command->setEnabled((int) $state);
+
+ $this->submittedFeatures[] = $command;
+
+ yield $command;
+ }
+ }
+ }
+}
diff --git a/application/forms/DatabaseConfigForm.php b/application/forms/DatabaseConfigForm.php
new file mode 100644
index 0000000..7a6c1bd
--- /dev/null
+++ b/application/forms/DatabaseConfigForm.php
@@ -0,0 +1,33 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms;
+
+use Icinga\Data\ResourceFactory;
+use Icinga\Forms\ConfigForm;
+
+class DatabaseConfigForm extends ConfigForm
+{
+ public function init()
+ {
+ $this->setSubmitLabel(t('Save Changes'));
+ }
+
+ public function createElements(array $formData)
+ {
+ $dbResources = ResourceFactory::getResourceConfigs('db')->keys();
+
+ $this->addElement('select', 'icingadb_resource', [
+ 'description' => t('Database resource'),
+ 'label' => t('Database'),
+ 'multiOptions' => array_merge(
+ ['' => sprintf(' - %s - ', t('Please choose'))],
+ array_combine($dbResources, $dbResources)
+ ),
+ 'disable' => [''],
+ 'required' => true,
+ 'value' => ''
+ ]);
+ }
+}
diff --git a/application/forms/Navigation/ActionForm.php b/application/forms/Navigation/ActionForm.php
new file mode 100644
index 0000000..08cba3f
--- /dev/null
+++ b/application/forms/Navigation/ActionForm.php
@@ -0,0 +1,58 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Navigation;
+
+use Icinga\Exception\ConfigurationError;
+use Icinga\Forms\Navigation\NavigationItemForm;
+use Icinga\Module\Icingadb\Common\Auth;
+
+class ActionForm extends NavigationItemForm
+{
+ use Auth;
+
+ /**
+ * The name of the restriction to which the filter should be applied
+ *
+ * @var string
+ */
+ protected $restriction;
+
+ public function createElements(array $formData)
+ {
+ parent::createElements($formData);
+
+ $this->addElement(
+ 'text',
+ 'filter',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Filter'),
+ 'description' => $this->translate(
+ 'Display this action only for objects matching this filter. Leave it blank'
+ . ' if you want this action being displayed regardless of the object'
+ )
+ )
+ );
+ }
+
+ public function isValid($formData): bool
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if (($filterString = $this->getValue('filter')) !== null) {
+ try {
+ $this->parseRestriction($filterString, $this->restriction);
+ } catch (ConfigurationError $err) {
+ $this->getElement('filter')->addError($err->getMessage());
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/application/forms/Navigation/IcingadbHostActionForm.php b/application/forms/Navigation/IcingadbHostActionForm.php
new file mode 100644
index 0000000..adee11d
--- /dev/null
+++ b/application/forms/Navigation/IcingadbHostActionForm.php
@@ -0,0 +1,10 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Navigation;
+
+class IcingadbHostActionForm extends ActionForm
+{
+ protected $restriction = 'icingadb/filter/hosts';
+}
diff --git a/application/forms/Navigation/IcingadbServiceActionForm.php b/application/forms/Navigation/IcingadbServiceActionForm.php
new file mode 100644
index 0000000..29d33c8
--- /dev/null
+++ b/application/forms/Navigation/IcingadbServiceActionForm.php
@@ -0,0 +1,10 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms\Navigation;
+
+class IcingadbServiceActionForm extends ActionForm
+{
+ protected $restriction = 'icingadb/filter/services';
+}
diff --git a/application/forms/RedisConfigForm.php b/application/forms/RedisConfigForm.php
new file mode 100644
index 0000000..bd3db9c
--- /dev/null
+++ b/application/forms/RedisConfigForm.php
@@ -0,0 +1,606 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms;
+
+use Closure;
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Exception\NotWritableError;
+use Icinga\File\Storage\LocalFileStorage;
+use Icinga\File\Storage\TemporaryLocalFileStorage;
+use Icinga\Forms\ConfigForm;
+use Icinga\Module\Icingadb\Common\IcingaRedis;
+use Icinga\Web\Form;
+use ipl\Validator\PrivateKeyValidator;
+use ipl\Validator\X509CertValidator;
+use Zend_Validate_Callback;
+
+class RedisConfigForm extends ConfigForm
+{
+ public function init()
+ {
+ $this->setSubmitLabel(t('Save Changes'));
+ $this->setValidatePartial(true);
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElement('checkbox', 'redis_tls', [
+ 'label' => t('Use TLS'),
+ 'description' => t('Encrypt connections to Redis via TLS'),
+ 'autosubmit' => true
+ ]);
+
+ $this->addElement('hidden', 'redis_ca');
+ $this->addElement('hidden', 'redis_cert');
+ $this->addElement('hidden', 'redis_key');
+ $this->addElement('hidden', 'clear_redis_ca', ['ignore' => true]);
+ $this->addElement('hidden', 'clear_redis_cert', ['ignore' => true]);
+ $this->addElement('hidden', 'clear_redis_key', ['ignore' => true]);
+
+ $useTls = isset($formData['redis_tls']) && $formData['redis_tls'];
+ if ($useTls) {
+ $this->addElement('textarea', 'redis_ca_pem', [
+ 'label' => t('Redis CA Certificate'),
+ 'description' => sprintf(
+ t('Verify the peer using this PEM-encoded CA certificate ("%s...")'),
+ '-----BEGIN CERTIFICATE-----'
+ ),
+ 'required' => true,
+ 'ignore' => true,
+ 'validators' => [$this->wrapIplValidator(X509CertValidator::class, 'redis_ca_pem')]
+ ]);
+
+ $this->addElement('textarea', 'redis_cert_pem', [
+ 'label' => t('Client Certificate'),
+ 'description' => sprintf(
+ t('Authenticate using this PEM-encoded client certificate ("%s...")'),
+ '-----BEGIN CERTIFICATE-----'
+ ),
+ 'ignore' => true,
+ 'allowEmpty' => false,
+ 'validators' => [
+ $this->wrapIplValidator(X509CertValidator::class, 'redis_cert_pem', function ($value) {
+ if (! $value && $this->getElement('redis_key_pem')->getValue()) {
+ $this->getElement('redis_cert_pem')->addError(t(
+ 'Either both a client certificate and its private key or none of them must be specified'
+ ));
+ }
+
+ return true;
+ })
+ ]
+ ]);
+
+ $this->addElement('textarea', 'redis_key_pem', [
+ 'label' => t('Client Key'),
+ 'description' => sprintf(
+ t('Authenticate using this PEM-encoded private key ("%s...")'),
+ '-----BEGIN PRIVATE KEY-----'
+ ),
+ 'ignore' => true,
+ 'allowEmpty' => false,
+ 'validators' => [
+ $this->wrapIplValidator(PrivateKeyValidator::class, 'redis_key_pem', function ($value) {
+ if (! $value && $this->getElement('redis_cert_pem')->getValue()) {
+ $this->getElement('redis_key_pem')->addError(t(
+ 'Either both a client certificate and its private key or none of them must be specified'
+ ));
+ }
+
+ return true;
+ })
+ ]
+ ]);
+ }
+
+ $this->addDisplayGroup(
+ ['redis_tls', 'redis_insecure', 'redis_ca_pem', 'redis_cert_pem', 'redis_key_pem'],
+ 'redis',
+ [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'div']],
+ [
+ 'Description',
+ ['tag' => 'span', 'class' => 'description', 'placement' => 'prepend']
+ ],
+ 'Fieldset'
+ ],
+ 'description' => t(
+ 'Secure connections. If you are running a high availability zone'
+ . ' with two masters, the following applies to both of them.'
+ ),
+ 'legend' => t('General')
+ ]
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ // In case another error occured and the checkbox was displayed before
+ static::addSkipValidationCheckbox($this);
+ }
+
+ if ($useTls && isset($formData['redis_insecure']) && $formData['redis_insecure']) {
+ // In case another error occured and the checkbox was displayed before
+ static::addInsecureCheckboxIfTls($this);
+ }
+
+ $this->addElement('text', 'redis1_host', [
+ 'description' => t('Redis Host'),
+ 'label' => t('Redis Host'),
+ 'required' => true
+ ]);
+
+ $this->addElement('number', 'redis1_port', [
+ 'description' => t('Redis Port'),
+ 'label' => t('Redis Port'),
+ 'placeholder' => 6380
+ ]);
+
+ $this->addElement('password', 'redis1_password', [
+ 'description' => t('Redis Password'),
+ 'label' => t('Redis Password'),
+ 'renderPassword' => true,
+ 'autocomplete' => 'new-password'
+ ]);
+
+ $this->addDisplayGroup(
+ ['redis1_host', 'redis1_port', 'redis1_password'],
+ 'redis1',
+ [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'div']],
+ [
+ 'Description',
+ ['tag' => 'span', 'class' => 'description', 'placement' => 'prepend']
+ ],
+ 'Fieldset'
+ ],
+ 'description' => t(
+ 'Redis connection details of your Icinga host. If you are running a high'
+ . ' availability zone with two masters, this is your configuration master.'
+ ),
+ 'legend' => t('Primary Icinga Master')
+ ]
+ );
+
+ $this->addElement('text', 'redis2_host', [
+ 'description' => t('Redis Host'),
+ 'label' => t('Redis Host'),
+ ]);
+
+ $this->addElement('number', 'redis2_port', [
+ 'description' => t('Redis Port'),
+ 'label' => t('Redis Port'),
+ 'placeholder' => 6380
+ ]);
+
+ $this->addElement('password', 'redis2_password', [
+ 'description' => t('Redis Password'),
+ 'label' => t('Redis Password'),
+ 'renderPassword' => true,
+ 'autocomplete' => 'new-password'
+ ]);
+
+ $this->addDisplayGroup(
+ ['redis2_host', 'redis2_port', 'redis2_password'],
+ 'redis2',
+ [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'div']],
+ [
+ 'Description',
+ ['tag' => 'span', 'class' => 'description', 'placement' => 'prepend']
+ ],
+ 'Fieldset'
+ ],
+ 'description' => t(
+ 'If you are running a high availability zone with two masters,'
+ . ' please provide the Redis connection details of the secondary master.'
+ ),
+ 'legend' => t('Secondary Icinga Master')
+ ]
+ );
+ }
+
+ public static function addSkipValidationCheckbox(Form $form)
+ {
+ $form->addElement(
+ 'checkbox',
+ 'skip_validation',
+ [
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => t('Skip Validation'),
+ 'description' => t(
+ 'Check this box to enforce changes without validating that Redis is available.'
+ )
+ ]
+ );
+ }
+
+ public static function addInsecureCheckboxIfTls(Form $form)
+ {
+ if ($form->getElement('redis_insecure') !== null) {
+ return;
+ }
+
+ $form->addElement(
+ 'checkbox',
+ 'redis_insecure',
+ [
+ 'order' => 1,
+ 'label' => t('Insecure'),
+ 'description' => t('Don\'t verify the peer')
+ ]
+ );
+
+ $displayGroup = $form->getDisplayGroup('redis');
+ $elements = $displayGroup->getElements();
+ $elements['redis_insecure'] = $form->getElement('redis_insecure');
+ $displayGroup->setElements($elements);
+ }
+
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if (($el = $this->getElement('skip_validation')) === null || ! $el->isChecked()) {
+ if (! static::checkRedis($this)) {
+ if ($el === null) {
+ static::addSkipValidationCheckbox($this);
+
+ if ($this->getElement('redis_tls')->isChecked()) {
+ static::addInsecureCheckboxIfTls($this);
+ }
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function isValidPartial(array $formData)
+ {
+ if (! parent::isValidPartial($formData)) {
+ return false;
+ }
+
+ $useTls = $this->getElement('redis_tls')->isChecked();
+ foreach (['ca', 'cert', 'key'] as $name) {
+ $textareaName = 'redis_' . $name . '_pem';
+ $clearName = 'clear_redis_' . $name;
+
+ if ($useTls) {
+ $this->getElement($clearName)->setValue(null);
+
+ $pemPath = $this->getValue('redis_' . $name);
+ if ($pemPath && ! isset($formData[$textareaName]) && ! $formData[$clearName]) {
+ $this->getElement($textareaName)->setValue(@file_get_contents($pemPath));
+ }
+ }
+
+ if (isset($formData[$textareaName]) && ! $formData[$textareaName]) {
+ $this->getElement($clearName)->setValue(true);
+ }
+ }
+
+ if ($this->getElement('backend_validation')->isChecked()) {
+ if (! static::checkRedis($this)) {
+ if ($this->getElement('redis_tls')->isChecked()) {
+ static::addInsecureCheckboxIfTls($this);
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function onRequest()
+ {
+ $errors = [];
+
+ $redisConfig = $this->config->getSection('redis');
+ if ($redisConfig->get('tls', false)) {
+ foreach (['ca', 'cert', 'key'] as $name) {
+ $path = $redisConfig->get($name);
+ if (file_exists($path)) {
+ try {
+ $redisConfig[$name . '_pem'] = file_get_contents($path);
+ } catch (Exception $e) {
+ $errors['redis_' . $name . '_pem'] = sprintf(
+ t('Failed to read file "%s": %s'),
+ $path,
+ $e->getMessage()
+ );
+ }
+ }
+ }
+ }
+
+ $connectionConfig = Config::fromIni(
+ join(DIRECTORY_SEPARATOR, [dirname($this->config->getConfigFile()), 'redis.ini'])
+ );
+ $this->config->setSection('redis1', [
+ 'host' => $connectionConfig->get('redis1', 'host'),
+ 'port' => $connectionConfig->get('redis1', 'port'),
+ 'password' => $connectionConfig->get('redis1', 'password')
+ ]);
+ $this->config->setSection('redis2', [
+ 'host' => $connectionConfig->get('redis2', 'host'),
+ 'port' => $connectionConfig->get('redis2', 'port'),
+ 'password' => $connectionConfig->get('redis2', 'password')
+ ]);
+
+ parent::onRequest();
+
+ foreach ($errors as $elementName => $message) {
+ $this->getElement($elementName)->addError($message);
+ }
+ }
+
+ public function onSuccess()
+ {
+ $storage = new LocalFileStorage(Icinga::app()->getStorageDir(
+ join(DIRECTORY_SEPARATOR, ['modules', 'icingadb', 'redis'])
+ ));
+
+ $useTls = $this->getElement('redis_tls')->isChecked();
+ $pem = null;
+ foreach (['ca', 'cert', 'key'] as $name) {
+ $textarea = $this->getElement('redis_' . $name . '_pem');
+ if ($useTls && $textarea !== null && ($pem = $textarea->getValue())) {
+ $pemFile = md5($pem) . '-' . $name . '.pem';
+ if (! $storage->has($pemFile)) {
+ try {
+ $storage->create($pemFile, $pem);
+ } catch (NotWritableError $e) {
+ $textarea->addError($e->getMessage());
+ return false;
+ }
+ }
+
+ $this->getElement('redis_' . $name)->setValue($storage->resolvePath($pemFile));
+ }
+
+ if ((! $useTls && $this->getElement('clear_redis_' . $name)->getValue()) || ($useTls && ! $pem)) {
+ $pemPath = $this->getValue('redis_' . $name);
+ if ($pemPath && $storage->has(($pemFile = basename($pemPath)))) {
+ try {
+ $storage->delete($pemFile);
+ $this->getElement('redis_' . $name)->setValue(null);
+ } catch (NotWritableError $e) {
+ $this->addError($e->getMessage());
+ return false;
+ }
+ }
+ }
+ }
+
+ $connectionConfig = Config::fromIni(
+ join(DIRECTORY_SEPARATOR, [dirname($this->config->getConfigFile()), 'redis.ini'])
+ );
+
+ $redis1Host = $this->getValue('redis1_host');
+ $redis1Port = $this->getValue('redis1_port');
+ $redis1Password = $this->getValue('redis1_password');
+ $redis1Section = $connectionConfig->getSection('redis1');
+ $redis1Section['host'] = $redis1Host;
+ $this->getElement('redis1_host')->setValue(null);
+ $connectionConfig->setSection('redis1', $redis1Section);
+ if (! empty($redis1Port)) {
+ $redis1Section['port'] = $redis1Port;
+ $this->getElement('redis1_port')->setValue(null);
+ } else {
+ $redis1Section['port'] = null;
+ }
+
+ if (! empty($redis1Password)) {
+ $redis1Section['password'] = $redis1Password;
+ $this->getElement('redis1_password')->setValue(null);
+ } else {
+ $redis1Section['password'] = null;
+ }
+
+ if (! array_filter($redis1Section->toArray())) {
+ $connectionConfig->removeSection('redis1');
+ }
+
+ $redis2Host = $this->getValue('redis2_host');
+ $redis2Port = $this->getValue('redis2_port');
+ $redis2Password = $this->getValue('redis2_password');
+ $redis2Section = $connectionConfig->getSection('redis2');
+ if (! empty($redis2Host)) {
+ $redis2Section['host'] = $redis2Host;
+ $this->getElement('redis2_host')->setValue(null);
+ $connectionConfig->setSection('redis2', $redis2Section);
+ } else {
+ $redis2Section['host'] = null;
+ }
+
+ if (! empty($redis2Port)) {
+ $redis2Section['port'] = $redis2Port;
+ $this->getElement('redis2_port')->setValue(null);
+ $connectionConfig->setSection('redis2', $redis2Section);
+ } else {
+ $redis2Section['port'] = null;
+ }
+
+ if (! empty($redis2Password)) {
+ $redis2Section['password'] = $redis2Password;
+ $this->getElement('redis2_password')->setValue(null);
+ } else {
+ $redis2Section['password'] = null;
+ }
+
+ if (! array_filter($redis2Section->toArray())) {
+ $connectionConfig->removeSection('redis2');
+ }
+
+ $connectionConfig->saveIni();
+
+ return parent::onSuccess();
+ }
+
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(['ViewHelper']);
+
+ $this->addElement(
+ 'submit',
+ 'backend_validation',
+ [
+ 'ignore' => true,
+ 'label' => t('Validate Configuration'),
+ 'data-progress-label' => t('Validation In Progress'),
+ 'decorators' => ['ViewHelper']
+ ]
+ );
+ $this->addDisplayGroup(
+ ['btn_submit', 'backend_validation'],
+ 'submit_validation',
+ [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']]
+ ]
+ ]
+ );
+
+ return $this;
+ }
+
+ public static function checkRedis(Form $form): bool
+ {
+ $sections = [];
+
+ $storage = new TemporaryLocalFileStorage();
+ foreach (ConfigForm::transformEmptyValuesToNull($form->getValues()) as $sectionAndPropertyName => $value) {
+ if ($value !== null) {
+ list($section, $property) = explode('_', $sectionAndPropertyName, 2);
+ if (in_array($property, ['ca', 'cert', 'key'])) {
+ $storage->create("$property.pem", $value);
+ $value = $storage->resolvePath("$property.pem");
+ }
+
+ $sections[$section][$property] = $value;
+ }
+ }
+
+ $ignoredTextAreas = [
+ 'ca' => 'redis_ca_pem',
+ 'cert' => 'redis_cert_pem',
+ 'key' => 'redis_key_pem'
+ ];
+ foreach ($ignoredTextAreas as $name => $textareaName) {
+ if (($textarea = $form->getElement($textareaName)) !== null) {
+ if (($pem = $textarea->getValue())) {
+ if ($storage->has("$name.pem")) {
+ $storage->update("$name.pem", $pem);
+ } else {
+ $storage->create("$name.pem", $pem);
+ $sections['redis'][$name] = $storage->resolvePath("$name.pem");
+ }
+ } elseif ($storage->has("$name.pem")) {
+ $storage->delete("$name.pem");
+ unset($sections['redis'][$name]);
+ }
+ }
+ }
+
+ $moduleConfig = new Config();
+ $moduleConfig->setSection('redis', $sections['redis']);
+ $redisConfig = new Config();
+ $redisConfig->setSection('redis1', $sections['redis1'] ?? []);
+ $redisConfig->setSection('redis2', $sections['redis2'] ?? []);
+
+ try {
+ $redis1 = IcingaRedis::getPrimaryRedis($moduleConfig, $redisConfig);
+ } catch (Exception $e) {
+ $form->warning(sprintf(
+ t('Failed to connect to primary Redis: %s'),
+ $e->getMessage()
+ ));
+ return false;
+ }
+
+ if (IcingaRedis::getLastIcingaHeartbeat($redis1) === null) {
+ $form->warning(t('Primary connection established but failed to verify Icinga is connected as well.'));
+ return false;
+ }
+
+ try {
+ $redis2 = IcingaRedis::getSecondaryRedis($moduleConfig, $redisConfig);
+ } catch (Exception $e) {
+ $form->warning(sprintf(t('Failed to connect to secondary Redis: %s'), $e->getMessage()));
+ return false;
+ }
+
+ if ($redis2 !== null && IcingaRedis::getLastIcingaHeartbeat($redis2) === null) {
+ $form->warning(t('Secondary connection established but failed to verify Icinga is connected as well.'));
+ return false;
+ }
+
+ $form->info(t('The configuration has been successfully validated.'));
+ return true;
+ }
+
+ /**
+ * Wraps the given IPL validator class into a callback validator
+ * for usage as the only validator of the element given by name.
+ *
+ * @param string $cls IPL validator class FQN
+ * @param string $element Form element name
+ * @param Closure $additionalValidator
+ *
+ * @return array Callback validator
+ */
+ private function wrapIplValidator(string $cls, string $element, Closure $additionalValidator = null): array
+ {
+ return [
+ 'Callback',
+ false,
+ [
+ 'callback' => function ($v) use ($cls, $element, $additionalValidator) {
+ if ($additionalValidator !== null) {
+ if (! $additionalValidator($v)) {
+ return false;
+ }
+ }
+
+ if (! $v) {
+ return true;
+ }
+
+ $validator = new $cls();
+ $valid = $validator->isValid($v);
+
+ if (! $valid) {
+ /** @var Zend_Validate_Callback $callbackValidator */
+ $callbackValidator = $this->getElement($element)->getValidator('Callback');
+
+ $callbackValidator->setMessage(
+ $validator->getMessages()[0],
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+ }
+
+ return $valid;
+ }
+ ]
+ ];
+ }
+}
diff --git a/application/forms/SetAsBackendForm.php b/application/forms/SetAsBackendForm.php
new file mode 100644
index 0000000..2840633
--- /dev/null
+++ b/application/forms/SetAsBackendForm.php
@@ -0,0 +1,34 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Forms;
+
+use Icinga\Module\Icingadb\Hook\IcingadbSupportHook;
+use Icinga\Web\Session;
+use ipl\Web\Compat\CompatForm;
+
+class SetAsBackendForm extends CompatForm
+{
+ protected $defaultAttributes = [
+ 'id' => 'setAsBackendForm',
+ 'class' => 'icinga-controls'
+ ];
+
+ protected function assemble()
+ {
+ $this->addElement('checkbox', 'backend', [
+ 'class' => 'autosubmit',
+ 'label' => t('Use Icinga DB As Backend'),
+ 'value' => IcingadbSupportHook::isIcingaDbSetAsPreferredBackend()
+ ]);
+ }
+
+ public function onSuccess()
+ {
+ Session::getSession()->getNamespace('icingadb')->set(
+ IcingadbSupportHook::PREFERENCE_NAME,
+ $this->getElement('backend')->isChecked()
+ );
+ }
+}