2026-03-23 04:26:36 -04:00
|
|
|
<?php
|
2026-04-02 01:25:46 -04:00
|
|
|
|
|
|
|
|
// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
|
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
2026-03-23 04:26:36 -04:00
|
|
|
|
|
|
|
|
namespace Icinga\Web\Form;
|
|
|
|
|
|
|
|
|
|
use Exception;
|
|
|
|
|
use Icinga\Application\Config;
|
2026-03-26 05:16:29 -04:00
|
|
|
use Icinga\Exception\ProgrammingError;
|
2026-03-23 04:26:36 -04:00
|
|
|
use Icinga\Web\Widget\ShowConfiguration;
|
2026-03-26 05:16:29 -04:00
|
|
|
use ipl\Html\Contract\FormSubmitElement;
|
|
|
|
|
use ipl\Validator\CallbackValidator;
|
2026-03-23 04:26:36 -04:00
|
|
|
use ipl\Web\Compat\CompatForm;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Form base-class providing standard functionality for configuration forms
|
|
|
|
|
*/
|
|
|
|
|
class ConfigForm extends CompatForm
|
|
|
|
|
{
|
2026-03-26 05:16:29 -04:00
|
|
|
/** @var string Event emitted when the form has successfully deleted a configuration section */
|
|
|
|
|
public const ON_DELETE = 'delete';
|
|
|
|
|
|
|
|
|
|
/** @var string Name of the submit button element */
|
|
|
|
|
protected const SUBMIT_BUTTON_NAME = 'store';
|
|
|
|
|
|
|
|
|
|
/** @var string Name of the delete button element */
|
|
|
|
|
protected const DELETE_BUTTON_NAME = 'delete';
|
|
|
|
|
|
|
|
|
|
/** @var string Name of the element containing the section name */
|
|
|
|
|
protected const NAME_ELEMENT_NAME = 'name';
|
|
|
|
|
|
2026-03-23 04:26:36 -04:00
|
|
|
/**
|
|
|
|
|
* A list of elements that should not be saved to the configuration
|
|
|
|
|
*
|
|
|
|
|
* @var string[]
|
|
|
|
|
*/
|
2026-03-26 05:16:29 -04:00
|
|
|
protected array $ignoredElements = ['name', 'store', 'delete'];
|
2026-03-23 04:26:36 -04:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The configuration to work with
|
|
|
|
|
*
|
|
|
|
|
* @var Config|null
|
|
|
|
|
*/
|
|
|
|
|
protected ?Config $config = null;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The section to work with.
|
|
|
|
|
* If not set, the section is determined from the element name.
|
|
|
|
|
*
|
|
|
|
|
* @var string|null
|
|
|
|
|
*/
|
|
|
|
|
protected ?string $section = null;
|
|
|
|
|
|
2026-03-26 05:16:29 -04:00
|
|
|
/**
|
|
|
|
|
* Whether the form is used for creating a new configuration section
|
|
|
|
|
*
|
|
|
|
|
* @var bool
|
|
|
|
|
*/
|
|
|
|
|
protected bool $isCreateForm = false;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Whether the form allows deletion of the configuration section
|
|
|
|
|
*
|
|
|
|
|
* @var bool
|
|
|
|
|
*/
|
|
|
|
|
protected bool $allowDeletion = true;
|
|
|
|
|
|
|
|
|
|
public function __construct()
|
|
|
|
|
{
|
|
|
|
|
$this->on(static::ON_SENT, function () {
|
|
|
|
|
if ($this->shouldDelete()) {
|
|
|
|
|
$this->handleDelete();
|
|
|
|
|
$this->emit(static::ON_DELETE, [$this]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isValidEvent($event): bool
|
|
|
|
|
{
|
|
|
|
|
// Check for our new event and return true if it is valid
|
|
|
|
|
if ($event === static::ON_DELETE) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Call the parent function to still validate all previous added events
|
|
|
|
|
return parent::isValidEvent($event);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 04:26:36 -04:00
|
|
|
/**
|
|
|
|
|
* Set the configuration to use when populating and saving
|
|
|
|
|
*
|
|
|
|
|
* @param Config $config The configuration to use
|
|
|
|
|
*
|
|
|
|
|
* @return $this
|
|
|
|
|
*/
|
|
|
|
|
public function setConfig(Config $config): static
|
|
|
|
|
{
|
|
|
|
|
$this->config = $config;
|
|
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set the section to use when populating and saving
|
|
|
|
|
*
|
|
|
|
|
* @param string $section The section to use
|
|
|
|
|
*
|
|
|
|
|
* @return $this
|
|
|
|
|
*/
|
|
|
|
|
public function setSection(string $section): static
|
|
|
|
|
{
|
|
|
|
|
$this->section = $section;
|
|
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 05:16:29 -04:00
|
|
|
/**
|
|
|
|
|
* Set whether the form is used for creating a new configuration section, with a name that can be chosen by the user
|
|
|
|
|
*
|
|
|
|
|
* @param bool $create
|
|
|
|
|
*
|
2026-04-02 01:28:03 -04:00
|
|
|
* @return static
|
2026-03-26 05:16:29 -04:00
|
|
|
*/
|
2026-04-02 01:28:03 -04:00
|
|
|
public function setIsCreateForm(bool $create = true): static
|
2026-03-26 05:16:29 -04:00
|
|
|
{
|
|
|
|
|
$this->isCreateForm = $create;
|
2026-04-02 01:28:03 -04:00
|
|
|
|
|
|
|
|
return $this;
|
2026-03-26 05:16:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Is the form used for creating a new configuration section
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function isCreateForm(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->isCreateForm;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set whether the form allows deletion of the configuration section
|
|
|
|
|
*
|
|
|
|
|
* @param bool $allowDeletion
|
|
|
|
|
*
|
2026-04-02 01:28:03 -04:00
|
|
|
* @return static
|
2026-03-26 05:16:29 -04:00
|
|
|
*/
|
2026-04-02 01:28:03 -04:00
|
|
|
public function setAllowDeletion(bool $allowDeletion = true): static
|
2026-03-26 05:16:29 -04:00
|
|
|
{
|
|
|
|
|
$this->allowDeletion = $allowDeletion;
|
2026-04-02 01:28:03 -04:00
|
|
|
|
|
|
|
|
return $this;
|
2026-03-26 05:16:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Is the form allowed to delete the configuration section.
|
|
|
|
|
* Note: Creation forms are never allowed to be deleted.
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function allowDeletion(): bool
|
|
|
|
|
{
|
|
|
|
|
if ($this->isCreateForm()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->allowDeletion;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 04:26:36 -04:00
|
|
|
public function ensureAssembled(): static
|
|
|
|
|
{
|
|
|
|
|
if (! $this->hasBeenAssembled) {
|
|
|
|
|
parent::ensureAssembled();
|
|
|
|
|
$this->populateFromConfig();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Populate the form elements from the configuration
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
protected function populateFromConfig(): void
|
|
|
|
|
{
|
|
|
|
|
foreach ($this->getElements() as $element) {
|
|
|
|
|
[$section, $key] = $this->getIniKeyFromName($element->getName());
|
|
|
|
|
if ($section === null || $key === null) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$value = $this->getPopulatedValue($element->getName()) ?? $this->config->get($section, $key);
|
|
|
|
|
$this->populate([
|
|
|
|
|
$element->getName() => $value,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the section and key from the element name.
|
|
|
|
|
* If `$this->section` it is used as the section name, with the key being the element name.
|
|
|
|
|
* Otherwise, the section is determined from the element name.
|
|
|
|
|
*
|
|
|
|
|
* @param string $name The element name
|
|
|
|
|
*
|
|
|
|
|
* @return string[]|null
|
|
|
|
|
*/
|
|
|
|
|
protected function getIniKeyFromName(string $name): ?array
|
|
|
|
|
{
|
2026-03-26 05:16:29 -04:00
|
|
|
$parts = explode('__', $name, 2);
|
2026-03-23 04:26:36 -04:00
|
|
|
if ($this->section !== null) {
|
2026-03-26 05:16:29 -04:00
|
|
|
if (count($parts) > 1) {
|
|
|
|
|
throw new ProgrammingError('Element name must not contain "__" when section is set.');
|
|
|
|
|
}
|
2026-03-23 04:26:36 -04:00
|
|
|
return [$this->section, $name];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (count($parts) !== 2) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $parts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the value of a configuration key from an element name
|
|
|
|
|
*
|
|
|
|
|
* @param string $name The element name
|
|
|
|
|
* @param mixed $default The default value to return if the element does not exist or the value is empty
|
|
|
|
|
*
|
|
|
|
|
* @return mixed The value of the configuration key or the default value
|
|
|
|
|
*/
|
|
|
|
|
public function getConfigValue(string $name, mixed $default = null): mixed
|
|
|
|
|
{
|
|
|
|
|
if (! $this->hasElement($name)) {
|
|
|
|
|
return $default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (($value = $this->getPopulatedValue($name)) !== null) {
|
|
|
|
|
return $value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[$section, $key] = $this->getIniKeyFromName($name);
|
|
|
|
|
return $this->config->get($section, $key, $default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Persist the current configuration to disk
|
|
|
|
|
*
|
|
|
|
|
* If an error occurs, the form will be re-rendered with the error message and the raw INI configuration.
|
|
|
|
|
*/
|
|
|
|
|
protected function save(): void
|
|
|
|
|
{
|
|
|
|
|
foreach ($this->getElements() as $element) {
|
|
|
|
|
if (in_array($element->getName(), $this->ignoredElements)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
[$section, $key] = $this->getIniKeyFromName($element->getName());
|
|
|
|
|
if ($section === null || $key === null) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$value = $this->getConfigValue($element->getName());
|
|
|
|
|
|
|
|
|
|
$configSection = $this->config->getSection($section);
|
|
|
|
|
if (empty($value)) {
|
|
|
|
|
unset($configSection[$key]);
|
|
|
|
|
} else {
|
|
|
|
|
$configSection->$key = $value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($configSection->isEmpty()) {
|
|
|
|
|
$this->config->removeSection($section);
|
|
|
|
|
} else {
|
|
|
|
|
$this->config->setSection($section, $configSection);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->config->saveIni();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 05:16:29 -04:00
|
|
|
/**
|
|
|
|
|
* Handle the deletion of the configuration section
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
protected function handleDelete(): void
|
|
|
|
|
{
|
|
|
|
|
if ($this->section === null) {
|
|
|
|
|
throw new ProgrammingError('Section must be set before deleting a configuration section.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$this->config->removeSection($this->section);
|
|
|
|
|
$this->config->saveIni();
|
|
|
|
|
} catch (Exception $e) {
|
|
|
|
|
$content = $this->getContent();
|
|
|
|
|
array_unshift(
|
|
|
|
|
$content,
|
|
|
|
|
new ShowConfiguration(
|
|
|
|
|
$e,
|
|
|
|
|
$this->config,
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
$this->setContent($content);
|
|
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 04:26:36 -04:00
|
|
|
protected function onSuccess(): void
|
|
|
|
|
{
|
2026-03-26 05:16:29 -04:00
|
|
|
if ($this->isCreateForm()) {
|
|
|
|
|
$this->section = $this->getValue(static::NAME_ELEMENT_NAME);
|
|
|
|
|
|
|
|
|
|
if (empty($this->section)) {
|
|
|
|
|
throw new ProgrammingError('Section must be set before saving a new configuration section.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 04:26:36 -04:00
|
|
|
try {
|
|
|
|
|
$this->save();
|
|
|
|
|
} catch (Exception $e) {
|
|
|
|
|
$content = $this->getContent();
|
|
|
|
|
array_unshift(
|
|
|
|
|
$content,
|
|
|
|
|
new ShowConfiguration(
|
|
|
|
|
$e,
|
|
|
|
|
$this->config,
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
$this->setContent($content);
|
|
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-26 05:16:29 -04:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if the delete button has been pressed and the section should be deleted
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function shouldDelete(): bool
|
|
|
|
|
{
|
|
|
|
|
if (! $this->hasDeleteButton()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$deleteButton = $this->getElement(static::DELETE_BUTTON_NAME);
|
|
|
|
|
if (! ($deleteButton instanceof FormSubmitElement)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $deleteButton->hasBeenPressed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function hasDeleteButton(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->hasElement(static::DELETE_BUTTON_NAME);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add the section name element to the form. This element is used to create a new configuration section with the
|
|
|
|
|
* given name. The added element automatically validates that the name is unique within the configuration.
|
|
|
|
|
*
|
|
|
|
|
* @param array $params Additional parameters to pass to the element constructor
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
protected function addSectionNameElement(array $params = []): void
|
|
|
|
|
{
|
|
|
|
|
if (! $this->isCreateForm()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$params['required'] = true;
|
|
|
|
|
|
|
|
|
|
if (! isset($params['label'])) {
|
|
|
|
|
$params['label'] = $this->translate('Name');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$uniqueValidator = new CallbackValidator(function ($value, CallbackValidator $validator) {
|
|
|
|
|
if (empty($value)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->config->hasSection($value)) {
|
|
|
|
|
$validator->addMessage(t('An entry with this name already exists.'));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (! isset($params['validators'])) {
|
|
|
|
|
$params['validators'] = [];
|
|
|
|
|
}
|
|
|
|
|
$params['validators'][] = $uniqueValidator;
|
|
|
|
|
|
|
|
|
|
$this->addElement('text', static::NAME_ELEMENT_NAME, $params);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add the store and optionally the delete buttons to the form.
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
protected function addButtonElements(): void
|
|
|
|
|
{
|
|
|
|
|
$this->addElement('submit', static::SUBMIT_BUTTON_NAME, [
|
|
|
|
|
'label' => $this->translate('Store'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (! $this->allowDeletion()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$deleteButton = $this->createElement(
|
|
|
|
|
'submit',
|
|
|
|
|
static::DELETE_BUTTON_NAME,
|
|
|
|
|
[
|
|
|
|
|
'label' => $this->translate('Delete'),
|
|
|
|
|
'formnovalidate' => true,
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
$this->registerElement($deleteButton);
|
|
|
|
|
$this->getElement(static::SUBMIT_BUTTON_NAME)
|
|
|
|
|
->getWrapper()
|
|
|
|
|
->prepend($deleteButton);
|
|
|
|
|
}
|
2026-03-23 04:26:36 -04:00
|
|
|
}
|