From 6ae554308e377b29c17911a7640f163b9bb0213f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Mon, 23 Mar 2026 09:26:36 +0100 Subject: [PATCH 01/14] Add `CompatForm` based version of `ConfigForm` --- library/Icinga/Web/Form/ConfigForm.php | 195 ++++++++++++++++++ .../Icinga/Web/Widget/ShowConfiguration.php | 96 +++++++++ 2 files changed, 291 insertions(+) create mode 100644 library/Icinga/Web/Form/ConfigForm.php create mode 100644 library/Icinga/Web/Widget/ShowConfiguration.php diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php new file mode 100644 index 0000000000..4197eb7df9 --- /dev/null +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -0,0 +1,195 @@ +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; + } + + 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 + { + if ($this->section !== null) { + return [$this->section, $name]; + } + + $parts = explode('__', $name, 2); + 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); + if (! $this->config->hasSection($section)) { + return $default; + } + + 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(); + } + + protected function onSuccess(): void + { + try { + $this->save(); + } catch (Exception $e) { + $content = $this->getContent(); + array_unshift( + $content, + new ShowConfiguration( + $e, + $this->config, + ) + ); + $this->setContent($content); + throw $e; + } + } +} diff --git a/library/Icinga/Web/Widget/ShowConfiguration.php b/library/Icinga/Web/Widget/ShowConfiguration.php new file mode 100644 index 0000000000..93534c5fad --- /dev/null +++ b/library/Icinga/Web/Widget/ShowConfiguration.php @@ -0,0 +1,96 @@ +addHtml(HtmlElement::create( + 'h4', + null, + t('Saving Configuration Failed!'), + )); + + $this->addHtml(HtmlElement::create( + 'p', + null, + [ + sprintf( + t("The file %s couldn't be stored. (Error: '%s')"), + $this->config->getConfigFile(), + $this->exception->getMessage(), + ), + HtmlString::create('
'), + t('This could have one or more of the following reasons:'), + ], + )); + + $this->addHtml(HtmlElement::create( + 'ul', + null, + [ + HtmlElement::create('li', null, t("You don't have file-system permissions to write to the file")), + HtmlElement::create('li', null, t('Something went wrong while writing the file')), + HtmlElement::create( + 'li', + null, + t("There's an application error preventing you from persisting the configuration"), + ), + ], + )); + + $this->addHtml(HtmlElement::create( + 'p', + null, + [ + t( + 'Details can be found in the application log. ' . + "(If you don't have access to this log, call your administrator in this case)", + ), + HtmlString::create('
'), + t('In case you can access the file by yourself, you can open it and insert the config manually:'), + ], + )); + + $code = HtmlElement::create('code', null, (string) $this->config); + CopyToClipboard::attachTo($code); + + $this->addHtml(HtmlElement::create( + 'p', + null, + HtmlElement::create( + 'pre', + null, + $code, + ), + )); + } +} From 3b9a9b6cf2584d77fe1c819bfe14c3de7b8681ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 26 Mar 2026 10:16:29 +0100 Subject: [PATCH 02/14] Expand `ConfigForm` to also allow usage as create and delete form --- library/Icinga/Web/Form/ConfigForm.php | 235 ++++++++++++++++++++++++- 1 file changed, 233 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php index 4197eb7df9..485e18b027 100644 --- a/library/Icinga/Web/Form/ConfigForm.php +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -5,7 +5,10 @@ use Exception; use Icinga\Application\Config; +use Icinga\Exception\ProgrammingError; use Icinga\Web\Widget\ShowConfiguration; +use ipl\Html\Contract\FormSubmitElement; +use ipl\Validator\CallbackValidator; use ipl\Web\Compat\CompatForm; /** @@ -13,12 +16,24 @@ */ class ConfigForm extends CompatForm { + /** @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'; + /** * A list of elements that should not be saved to the configuration * * @var string[] */ - protected array $ignoredElements = []; + protected array $ignoredElements = ['name', 'store', 'delete']; /** * The configuration to work with @@ -35,6 +50,41 @@ class ConfigForm extends CompatForm */ protected ?string $section = null; + /** + * 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); + } + /** * Set the configuration to use when populating and saving * @@ -63,6 +113,54 @@ public function setSection(string $section): static return $this; } + /** + * 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 + * + * @return void + */ + public function setIsCreateForm(bool $create = true): void + { + $this->isCreateForm = $create; + } + + /** + * 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 + * + * @return void + */ + public function setAllowDeletion(bool $allowDeletion = true): void + { + $this->allowDeletion = $allowDeletion; + } + + /** + * 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; + } + public function ensureAssembled(): static { if (! $this->hasBeenAssembled) { @@ -103,11 +201,14 @@ protected function populateFromConfig(): void */ protected function getIniKeyFromName(string $name): ?array { + $parts = explode('__', $name, 2); if ($this->section !== null) { + if (count($parts) > 1) { + throw new ProgrammingError('Element name must not contain "__" when section is set.'); + } return [$this->section, $name]; } - $parts = explode('__', $name, 2); if (count($parts) !== 2) { return null; } @@ -175,8 +276,44 @@ protected function save(): void $this->config->saveIni(); } + /** + * 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; + } + } + protected function onSuccess(): void { + 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.'); + } + } + try { $this->save(); } catch (Exception $e) { @@ -192,4 +329,98 @@ protected function onSuccess(): void throw $e; } } + + /** + * 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); + } } From ee4e674cf51dfeaacdd13a92f27051c8682b8faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 2 Apr 2026 07:25:46 +0200 Subject: [PATCH 03/14] Change license and add SPDX-header --- library/Icinga/Web/Form/ConfigForm.php | 4 +++- library/Icinga/Web/Widget/ShowConfiguration.php | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php index 485e18b027..7d740e7db3 100644 --- a/library/Icinga/Web/Form/ConfigForm.php +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -1,5 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Web\Form; diff --git a/library/Icinga/Web/Widget/ShowConfiguration.php b/library/Icinga/Web/Widget/ShowConfiguration.php index 93534c5fad..261906dd80 100644 --- a/library/Icinga/Web/Widget/ShowConfiguration.php +++ b/library/Icinga/Web/Widget/ShowConfiguration.php @@ -1,5 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Web\Widget; From cff26998334910ce97550b20749606808f60ec20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 2 Apr 2026 07:28:03 +0200 Subject: [PATCH 04/14] Use fluent setters --- library/Icinga/Web/Form/ConfigForm.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php index 7d740e7db3..cc82530526 100644 --- a/library/Icinga/Web/Form/ConfigForm.php +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -120,11 +120,13 @@ public function setSection(string $section): static * * @param bool $create * - * @return void + * @return static */ - public function setIsCreateForm(bool $create = true): void + public function setIsCreateForm(bool $create = true): static { $this->isCreateForm = $create; + + return $this; } /** @@ -142,11 +144,13 @@ public function isCreateForm(): bool * * @param bool $allowDeletion * - * @return void + * @return static */ - public function setAllowDeletion(bool $allowDeletion = true): void + public function setAllowDeletion(bool $allowDeletion = true): static { $this->allowDeletion = $allowDeletion; + + return $this; } /** From d444566c3081a4ec5cbe116fe33830f4ef13ee8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 2 Apr 2026 07:44:13 +0200 Subject: [PATCH 05/14] Remove superfluous hasSection call --- library/Icinga/Web/Form/ConfigForm.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php index cc82530526..4bb838b219 100644 --- a/library/Icinga/Web/Form/ConfigForm.php +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -241,10 +241,6 @@ public function getConfigValue(string $name, mixed $default = null): mixed } [$section, $key] = $this->getIniKeyFromName($name); - if (! $this->config->hasSection($section)) { - return $default; - } - return $this->config->get($section, $key, $default); } From 9efc29c5f9ab597fbd99f7e5af3c30545f69782b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 16 Apr 2026 11:20:22 +0200 Subject: [PATCH 06/14] Split section handling of ConfigForm into new ConfigSectionForm --- library/Icinga/Web/Form/ConfigForm.php | 251 +------------- library/Icinga/Web/Form/ConfigSectionForm.php | 307 ++++++++++++++++++ 2 files changed, 309 insertions(+), 249 deletions(-) create mode 100644 library/Icinga/Web/Form/ConfigSectionForm.php diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php index 4bb838b219..95aacacbb6 100644 --- a/library/Icinga/Web/Form/ConfigForm.php +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -7,10 +7,7 @@ use Exception; use Icinga\Application\Config; -use Icinga\Exception\ProgrammingError; use Icinga\Web\Widget\ShowConfiguration; -use ipl\Html\Contract\FormSubmitElement; -use ipl\Validator\CallbackValidator; use ipl\Web\Compat\CompatForm; /** @@ -18,24 +15,15 @@ */ class ConfigForm extends CompatForm { - /** @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'; - /** * A list of elements that should not be saved to the configuration * * @var string[] */ - protected array $ignoredElements = ['name', 'store', 'delete']; + protected array $ignoredElements = [self::SUBMIT_BUTTON_NAME]; /** * The configuration to work with @@ -44,49 +32,6 @@ class ConfigForm extends CompatForm */ 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; - - /** - * 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); - } - /** * Set the configuration to use when populating and saving * @@ -101,72 +46,6 @@ public function setConfig(Config $config): static 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; - } - - /** - * 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 - * - * @return static - */ - public function setIsCreateForm(bool $create = true): static - { - $this->isCreateForm = $create; - - return $this; - } - - /** - * 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 - * - * @return static - */ - public function setAllowDeletion(bool $allowDeletion = true): static - { - $this->allowDeletion = $allowDeletion; - - return $this; - } - - /** - * 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; - } - public function ensureAssembled(): static { if (! $this->hasBeenAssembled) { @@ -198,8 +77,6 @@ protected function populateFromConfig(): void /** * 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 * @@ -208,12 +85,6 @@ protected function populateFromConfig(): void protected function getIniKeyFromName(string $name): ?array { $parts = explode('__', $name, 2); - if ($this->section !== null) { - if (count($parts) > 1) { - throw new ProgrammingError('Element name must not contain "__" when section is set.'); - } - return [$this->section, $name]; - } if (count($parts) !== 2) { return null; @@ -278,44 +149,8 @@ protected function save(): void $this->config->saveIni(); } - /** - * 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; - } - } - protected function onSuccess(): void { - 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.'); - } - } - try { $this->save(); } catch (Exception $e) { @@ -333,72 +168,7 @@ protected function onSuccess(): void } /** - * 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. + * Add the store button to the form. * * @return void */ @@ -407,22 +177,5 @@ 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); } } diff --git a/library/Icinga/Web/Form/ConfigSectionForm.php b/library/Icinga/Web/Form/ConfigSectionForm.php new file mode 100644 index 0000000000..d24d81d8b2 --- /dev/null +++ b/library/Icinga/Web/Form/ConfigSectionForm.php @@ -0,0 +1,307 @@ +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); + } + + /** + * 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; + } + + /** + * 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 + * + * @return static + */ + public function setIsCreateForm(bool $create = true): static + { + $this->isCreateForm = $create; + + return $this; + } + + /** + * 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 + * + * @return static + */ + public function setAllowDeletion(bool $allowDeletion = true): static + { + $this->allowDeletion = $allowDeletion; + + return $this; + } + + /** + * 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; + } + + /** + * 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; + } + } + + /** + * 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); + } + + /** + * Get the section and key from the element name. + * + * @param string $name The element name + * + * @return string[]|null + */ + protected function getIniKeyFromName(string $name): ?array + { + return [$this->section, $name]; + } + + protected function onSuccess(): void + { + 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.'); + } + } + + parent::onSuccess(); + } + + /** + * Add the store and optionally the delete buttons to the form. + * + * @return void + */ + protected function addButtonElements(): void + { + parent::addButtonElements(); + + 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); + } +} From 6e382b69fe466ef7a6e3b64f1e24d4c5951ad73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 16 Apr 2026 13:22:07 +0200 Subject: [PATCH 07/14] Allow renming sections --- library/Icinga/Web/Form/ConfigSectionForm.php | 92 +++++++++++++++---- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/library/Icinga/Web/Form/ConfigSectionForm.php b/library/Icinga/Web/Form/ConfigSectionForm.php index d24d81d8b2..9afddf933d 100644 --- a/library/Icinga/Web/Form/ConfigSectionForm.php +++ b/library/Icinga/Web/Form/ConfigSectionForm.php @@ -23,6 +23,9 @@ class ConfigSectionForm extends ConfigForm /** @var string Event emitted when the form has successfully deleted a configuration section */ public const ON_DELETE = 'delete'; + /** @var string Event emitted when the form has successfully renamed a configuration section */ + public const ON_RENAME = 'rename'; + /** * A list of elements that should not be saved to the configuration * @@ -59,39 +62,49 @@ class ConfigSectionForm extends ConfigForm */ protected bool $allowDeletion = true; + /** + * Whether the form allows renaming of the configuration section + * + * @var bool + */ + protected bool $allowRename = true; + public function __construct() { $this->on(static::ON_SENT, function () { if ($this->shouldDelete()) { $this->handleDelete(); $this->emit(static::ON_DELETE, [$this]); + } else if ($this->shouldRename()) { + $oldName = $this->section; + $this->handleRename(); + $this->emit(static::ON_RENAME, [ + $this, + $oldName, + $this->section, + ]); } }); } - public function isValidEvent($event): bool + protected function populateFromConfig(): void { - // Check for our new event and return true if it is valid - if ($event === static::ON_DELETE) { - return true; + if ($this->allowRename()) { + $this->populate([ + static::NAME_ELEMENT_NAME => $this->getPopulatedValue(static::NAME_ELEMENT_NAME, $this->section), + ]); } - // Call the parent function to still validate all previous added events - return parent::isValidEvent($event); + parent::populateFromConfig(); } - /** - * Set the configuration to use when populating and saving - * - * @param Config $config The configuration to use - * - * @return $this - */ - public function setConfig(Config $config): static + public function isValidEvent($event): bool { - $this->config = $config; + if ($event === static::ON_DELETE || $event === static::ON_RENAME) { + return true; + } - return $this; + return parent::isValidEvent($event); } /** @@ -160,6 +173,22 @@ public function allowDeletion(): bool return $this->allowDeletion; } + public function setAllowRename(bool $allowRename = true): static + { + $this->allowRename = $allowRename; + + return $this; + } + + public function allowRename(): bool + { + if ($this->isCreateForm()) { + return false; + } + + return $this->allowRename; + } + /** * Handle the deletion of the configuration section * @@ -188,6 +217,22 @@ protected function handleDelete(): void } } + /** + * Handle the renaming of the configuration section + * + * @return void + */ + protected function handleRename(): void + { + $newName = $this->getPopulatedValue(static::NAME_ELEMENT_NAME); + $section = $this->config->getSection($this->section); + $this->config->removeSection($this->section); + $this->config->setSection($newName, $section); + $this->section = $newName; + + $this->onSuccess(); + } + /** * Check if the delete button has been pressed and the section should be deleted * @@ -222,7 +267,7 @@ public function hasDeleteButton(): bool */ protected function addSectionNameElement(array $params = []): void { - if (! $this->isCreateForm()) { + if (! $this->isCreateForm() && ! $this->allowRename()) { return; } @@ -237,6 +282,10 @@ protected function addSectionNameElement(array $params = []): void return true; } + if ($value == $this->section) { + return true; + } + if ($this->config->hasSection($value)) { $validator->addMessage(t('An entry with this name already exists.')); return false; @@ -304,4 +353,13 @@ protected function addButtonElements(): void ->getWrapper() ->prepend($deleteButton); } + + private function shouldRename(): bool + { + if (! $this->allowRename() || ! $this->hasBeenSubmitted()) { + return false; + } + + return $this->allowRename() && $this->section !== $this->getPopulatedValue(static::NAME_ELEMENT_NAME); + } } From 4711b41a5c952448659ef531b96da3d15be5d2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Thu, 16 Apr 2026 15:09:05 +0200 Subject: [PATCH 08/14] fixup! phpcs --- library/Icinga/Web/Form/ConfigSectionForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Web/Form/ConfigSectionForm.php b/library/Icinga/Web/Form/ConfigSectionForm.php index 9afddf933d..952d84c018 100644 --- a/library/Icinga/Web/Form/ConfigSectionForm.php +++ b/library/Icinga/Web/Form/ConfigSectionForm.php @@ -75,7 +75,7 @@ public function __construct() if ($this->shouldDelete()) { $this->handleDelete(); $this->emit(static::ON_DELETE, [$this]); - } else if ($this->shouldRename()) { + } elseif ($this->shouldRename()) { $oldName = $this->section; $this->handleRename(); $this->emit(static::ON_RENAME, [ From 5f76490044db9441110a48f3181e985ffaa0bc44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Mon, 11 May 2026 15:39:08 +0200 Subject: [PATCH 09/14] Only populate fields that actually have a value --- library/Icinga/Web/Form/ConfigForm.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php index 95aacacbb6..3e33c8db53 100644 --- a/library/Icinga/Web/Form/ConfigForm.php +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -63,16 +63,19 @@ public function ensureAssembled(): static */ protected function populateFromConfig(): void { + $populate = []; 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, - ]); + if ($value === null) { + continue; + } + $populate[$element->getName()] = $value; } + $this->populate($populate); } /** From 3dc7543b808f1b4340db6106173455c1c57b464b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Mon, 11 May 2026 16:12:34 +0200 Subject: [PATCH 10/14] Check against '' instad of empty() --- library/Icinga/Web/Form/ConfigForm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php index 3e33c8db53..555ef73fee 100644 --- a/library/Icinga/Web/Form/ConfigForm.php +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -133,10 +133,10 @@ protected function save(): void if ($section === null || $key === null) { continue; } - $value = $this->getConfigValue($element->getName()); + $value = $this->getPopulatedValue($element->getName()); $configSection = $this->config->getSection($section); - if (empty($value)) { + if ((string) $value === '') { unset($configSection[$key]); } else { $configSection->$key = $value; From e045445f558e5302d6362d22c38ce4a41477b44b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Mon, 11 May 2026 16:24:03 +0200 Subject: [PATCH 11/14] Cleanup --- library/Icinga/Web/Form/ConfigForm.php | 2 +- library/Icinga/Web/Form/ConfigSectionForm.php | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php index 555ef73fee..e88e397ff9 100644 --- a/library/Icinga/Web/Form/ConfigForm.php +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -90,7 +90,7 @@ protected function getIniKeyFromName(string $name): ?array $parts = explode('__', $name, 2); if (count($parts) !== 2) { - return null; + return [null, null]; } return $parts; diff --git a/library/Icinga/Web/Form/ConfigSectionForm.php b/library/Icinga/Web/Form/ConfigSectionForm.php index 952d84c018..852dad4c09 100644 --- a/library/Icinga/Web/Form/ConfigSectionForm.php +++ b/library/Icinga/Web/Form/ConfigSectionForm.php @@ -1,5 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + namespace Icinga\Web\Form; use Exception; @@ -17,9 +20,6 @@ class ConfigSectionForm extends ConfigForm /** @var string Name of the element containing the section name */ protected const NAME_ELEMENT_NAME = 'name'; - /** @var string Name of the submit button element */ - protected const SUBMIT_BUTTON_NAME = 'store'; - /** @var string Event emitted when the form has successfully deleted a configuration section */ public const ON_DELETE = 'delete'; @@ -33,13 +33,6 @@ class ConfigSectionForm extends ConfigForm */ protected array $ignoredElements = [self::SUBMIT_BUTTON_NAME, self::DELETE_BUTTON_NAME, self::NAME_ELEMENT_NAME]; - /** - * 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. @@ -192,6 +185,10 @@ public function allowRename(): bool /** * Handle the deletion of the configuration section * + * This method is called when the delete button is pressed. + * It deletes the underlying section regardless of whether form validation passed. + * This is done to allow for deletion of sections that contain invalid configuration. + * * @return void */ protected function handleDelete(): void @@ -220,6 +217,10 @@ protected function handleDelete(): void /** * Handle the renaming of the configuration section * + * This method is called when the rename button is pressed. + * It renames the underlying section and updates the section name in the form. + * Renaming is allowed regardless of whether form validation passed. + * * @return void */ protected function handleRename(): void @@ -258,8 +259,9 @@ public function hasDeleteButton(): bool } /** - * 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. + * 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 * @@ -360,6 +362,6 @@ private function shouldRename(): bool return false; } - return $this->allowRename() && $this->section !== $this->getPopulatedValue(static::NAME_ELEMENT_NAME); + return $this->section !== $this->getPopulatedValue(static::NAME_ELEMENT_NAME); } } From 44c4fa852a8f1c123579ec2fabd08625d3b99d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 12 May 2026 07:40:24 +0200 Subject: [PATCH 12/14] Force rename to also be a valid form submission --- library/Icinga/Web/Form/ConfigForm.php | 23 ++++++++++++++----- library/Icinga/Web/Form/ConfigSectionForm.php | 6 +---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php index e88e397ff9..c73dc5c522 100644 --- a/library/Icinga/Web/Form/ConfigForm.php +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -7,6 +7,7 @@ use Exception; use Icinga\Application\Config; +use Icinga\Exception\ProgrammingError; use Icinga\Web\Widget\ShowConfiguration; use ipl\Web\Compat\CompatForm; @@ -63,6 +64,10 @@ public function ensureAssembled(): static */ protected function populateFromConfig(): void { + if (! $this->config) { + throw new ProgrammingError("A config object must be set before populating the form."); + } + $populate = []; foreach ($this->getElements() as $element) { [$section, $key] = $this->getIniKeyFromName($element->getName()); @@ -106,15 +111,17 @@ protected function getIniKeyFromName(string $name): ?array */ public function getConfigValue(string $name, mixed $default = null): mixed { - if (! $this->hasElement($name)) { - return $default; - } + [$section, $key] = $this->getIniKeyFromName($name); + if ($section !== null && $key !== null) { + if (! $this->hasElement($name)) { + return $default; + } - if (($value = $this->getPopulatedValue($name)) !== null) { - return $value; + if (($value = $this->getPopulatedValue($name)) !== null) { + return $value; + } } - [$section, $key] = $this->getIniKeyFromName($name); return $this->config->get($section, $key, $default); } @@ -125,6 +132,10 @@ public function getConfigValue(string $name, mixed $default = null): mixed */ protected function save(): void { + if (! $this->config) { + throw new ProgrammingError("A config object must be set before saving the configuration."); + } + foreach ($this->getElements() as $element) { if (in_array($element->getName(), $this->ignoredElements)) { continue; diff --git a/library/Icinga/Web/Form/ConfigSectionForm.php b/library/Icinga/Web/Form/ConfigSectionForm.php index 852dad4c09..9453307f39 100644 --- a/library/Icinga/Web/Form/ConfigSectionForm.php +++ b/library/Icinga/Web/Form/ConfigSectionForm.php @@ -6,7 +6,6 @@ namespace Icinga\Web\Form; use Exception; -use Icinga\Application\Config; use Icinga\Exception\ProgrammingError; use Icinga\Web\Widget\ShowConfiguration; use ipl\Html\Contract\FormSubmitElement; @@ -219,7 +218,6 @@ protected function handleDelete(): void * * This method is called when the rename button is pressed. * It renames the underlying section and updates the section name in the form. - * Renaming is allowed regardless of whether form validation passed. * * @return void */ @@ -230,8 +228,6 @@ protected function handleRename(): void $this->config->removeSection($this->section); $this->config->setSection($newName, $section); $this->section = $newName; - - $this->onSuccess(); } /** @@ -358,7 +354,7 @@ protected function addButtonElements(): void private function shouldRename(): bool { - if (! $this->allowRename() || ! $this->hasBeenSubmitted()) { + if (! $this->allowRename() || ! $this->hasBeenSubmitted() || ! $this->isValid()) { return false; } From db42effad76b5da45b112fc6922ae0525e9dac15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 12 May 2026 08:11:28 +0200 Subject: [PATCH 13/14] Rename should only work on form submission --- library/Icinga/Web/Form/ConfigForm.php | 12 ++----- library/Icinga/Web/Form/ConfigSectionForm.php | 31 ++++++++++++++----- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php index c73dc5c522..2e394a9cb5 100644 --- a/library/Icinga/Web/Form/ConfigForm.php +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -105,21 +105,15 @@ protected function getIniKeyFromName(string $name): ?array * 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 + * @param mixed $default The default value to return if the config entry does not exist * * @return mixed The value of the configuration key or the default value */ public function getConfigValue(string $name, mixed $default = null): mixed { [$section, $key] = $this->getIniKeyFromName($name); - if ($section !== null && $key !== null) { - if (! $this->hasElement($name)) { - return $default; - } - - if (($value = $this->getPopulatedValue($name)) !== null) { - return $value; - } + if ($section === null || $key === null) { + return $default; } return $this->config->get($section, $key, $default); diff --git a/library/Icinga/Web/Form/ConfigSectionForm.php b/library/Icinga/Web/Form/ConfigSectionForm.php index 9453307f39..45e5ad7811 100644 --- a/library/Icinga/Web/Form/ConfigSectionForm.php +++ b/library/Icinga/Web/Form/ConfigSectionForm.php @@ -67,14 +67,6 @@ public function __construct() if ($this->shouldDelete()) { $this->handleDelete(); $this->emit(static::ON_DELETE, [$this]); - } elseif ($this->shouldRename()) { - $oldName = $this->section; - $this->handleRename(); - $this->emit(static::ON_RENAME, [ - $this, - $oldName, - $this->section, - ]); } }); } @@ -223,6 +215,14 @@ protected function handleDelete(): void */ protected function handleRename(): void { + if (! $this->config) { + throw new ProgrammingError('Config must be set before renaming a configuration section.'); + } + + if ($this->section === null) { + throw new ProgrammingError('Section must be set before renaming a configuration section.'); + } + $newName = $this->getPopulatedValue(static::NAME_ELEMENT_NAME); $section = $this->config->getSection($this->section); $this->config->removeSection($this->section); @@ -322,7 +322,22 @@ protected function onSuccess(): void } } + $oldSection = $this->section; + $isRename = $this->shouldRename(); + + if ($isRename) { + $this->handleRename(); + } + parent::onSuccess(); + + if ($isRename) { + $this->emit(static::ON_RENAME, [ + $this, + $oldSection, + $this->section, + ]); + } } /** From 94ee656c1a490c3f0a4e343bcaa65c2cea395b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 12 May 2026 08:38:11 +0200 Subject: [PATCH 14/14] Docstring formating --- library/Icinga/Web/Form/ConfigForm.php | 11 +++-- library/Icinga/Web/Form/ConfigSectionForm.php | 47 ++++++++++++------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/library/Icinga/Web/Form/ConfigForm.php b/library/Icinga/Web/Form/ConfigForm.php index 2e394a9cb5..d76d7e633a 100644 --- a/library/Icinga/Web/Form/ConfigForm.php +++ b/library/Icinga/Web/Form/ConfigForm.php @@ -61,6 +61,8 @@ public function ensureAssembled(): static * Populate the form elements from the configuration * * @return void + * + * @throws ProgrammingError */ protected function populateFromConfig(): void { @@ -84,7 +86,7 @@ protected function populateFromConfig(): void } /** - * Get the section and key from the element name. + * Get the section and key from the element name * * @param string $name The element name * @@ -122,7 +124,10 @@ public function getConfigValue(string $name, mixed $default = null): mixed /** * 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. + * If an error occurs, the form will be re-rendered with the error message + * and the raw INI configuration. + * + * @throws ProgrammingError */ protected function save(): void { @@ -176,7 +181,7 @@ protected function onSuccess(): void } /** - * Add the store button to the form. + * Add the store button to the form * * @return void */ diff --git a/library/Icinga/Web/Form/ConfigSectionForm.php b/library/Icinga/Web/Form/ConfigSectionForm.php index 45e5ad7811..ea4e9ddb56 100644 --- a/library/Icinga/Web/Form/ConfigSectionForm.php +++ b/library/Icinga/Web/Form/ConfigSectionForm.php @@ -144,8 +144,10 @@ public function setAllowDeletion(bool $allowDeletion = true): static } /** - * Is the form allowed to delete the configuration section. + * Whether the form is allowed to delete the configuration section + * * Note: Creation forms are never allowed to be deleted. + * * @return bool */ public function allowDeletion(): bool @@ -157,6 +159,13 @@ public function allowDeletion(): bool return $this->allowDeletion; } + /** + * Set the ability to rename the configuration section + * + * @param bool $allowRename Whether the form is allowed to rename the configuration section + * + * @return $this + */ public function setAllowRename(bool $allowRename = true): static { $this->allowRename = $allowRename; @@ -164,6 +173,13 @@ public function setAllowRename(bool $allowRename = true): static return $this; } + /** + * Whether the form is allowed to rename the configuration section + * + * Note: Creation forms are never allowed to be rename forms. + * + * @return bool + */ public function allowRename(): bool { if ($this->isCreateForm()) { @@ -181,6 +197,8 @@ public function allowRename(): bool * This is done to allow for deletion of sections that contain invalid configuration. * * @return void + * + * @throws ProgrammingError */ protected function handleDelete(): void { @@ -212,6 +230,8 @@ protected function handleDelete(): void * It renames the underlying section and updates the section name in the form. * * @return void + * + * @throws ProgrammingError */ protected function handleRename(): void { @@ -255,9 +275,11 @@ public function hasDeleteButton(): bool } /** - * 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. + * 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 * @@ -300,13 +322,6 @@ protected function addSectionNameElement(array $params = []): void $this->addElement('text', static::NAME_ELEMENT_NAME, $params); } - /** - * Get the section and key from the element name. - * - * @param string $name The element name - * - * @return string[]|null - */ protected function getIniKeyFromName(string $name): ?array { return [$this->section, $name]; @@ -340,11 +355,6 @@ protected function onSuccess(): void } } - /** - * Add the store and optionally the delete buttons to the form. - * - * @return void - */ protected function addButtonElements(): void { parent::addButtonElements(); @@ -367,6 +377,11 @@ protected function addButtonElements(): void ->prepend($deleteButton); } + /** + * Check if the form should rename the section for this request + * + * @return bool + */ private function shouldRename(): bool { if (! $this->allowRename() || ! $this->hasBeenSubmitted() || ! $this->isValid()) {