diff --git a/core/lib/Drupal/Core/Form/ConfigFormBase.php b/core/lib/Drupal/Core/Form/ConfigFormBase.php index 2931e4a3e24dab8a83a3a7a825a9072a6a2a853a..6436eaebe694714aec12d5b6e0bcc04fa72727fd 100644 --- a/core/lib/Drupal/Core/Form/ConfigFormBase.php +++ b/core/lib/Drupal/Core/Form/ConfigFormBase.php @@ -2,11 +2,13 @@ namespace Drupal\Core\Form; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Config\Config; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Render\Element; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -86,7 +88,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { // property. $form['#process'][] = '::loadDefaultValuesFromConfig'; $form['#after_build'][] = '::storeConfigKeyToFormElementMap'; - + $form['#after_build'][] = '::checkConfigOverrides'; return $form; } @@ -333,4 +335,58 @@ private static function copyFormValuesToConfig(Config $config, FormStateInterfac } } + /** + * Form #after_build callback: Adds message if overrides exist. + */ + public function checkConfigOverrides(array $form, FormStateInterface $form_state): array { + // Determine which of those editable config keys have overrides. + $override_links = []; + $map = $form_state->get(static::CONFIG_KEY_TO_FORM_ELEMENT_MAP) ?? []; + foreach ($map as $config_name => $config_keys) { + $stored_config = $this->configFactory->get($config_name); + if (!$stored_config->hasOverrides()) { + // The config has no overrides at all. Can be skipped. + continue; + } + + foreach ($config_keys as $key => $array_parents) { + if ($stored_config->hasOverrides($key)) { + $element = NestedArray::getValue($form, $array_parents); + $override_links[] = [ + 'attributes' => ['title' => $this->t("'@title' form element", ['@title' => $element['#title']])], + 'url' => Url::fromUri("internal:#{$element['#id']}"), + 'title' => $element['#title'], + ]; + } + } + } + + if (!empty($override_links)) { + $override_output = [ + '#theme' => 'links__config_overrides', + '#heading' => [ + 'text' => $this->t('These values are overridden. Changes on this form will be saved, but overrides will take precedence. See <a href="https://www.drupal.org/docs/drupal-apis/configuration-api/configuration-override-system">configuration overrides documentation</a> for more information.'), + 'level' => 'div', + ], + '#links' => $override_links, + ]; + $form['config_override_status_messages'] = [ + 'message' => [ + '#theme' => 'status_messages', + '#message_list' => ['status' => [$override_output]], + '#status_headings' => [ + 'status' => $this->t('Status message'), + ], + ], + // Ensure that the status message is at the top of the form. + '#weight' => array_reduce( + Element::children($form), + fn (int $carry, string $key) => min(($form[$key]['#weight'] ?? 0), $carry), + 0 + ) - 1, + ]; + } + return $form; + } + } diff --git a/core/modules/config/tests/config_override_message_test/config_override_message_test.info.yml b/core/modules/config/tests/config_override_message_test/config_override_message_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..2b4a23bfc46fbdb5b41052e92b10616a9938d9bf --- /dev/null +++ b/core/modules/config/tests/config_override_message_test/config_override_message_test.info.yml @@ -0,0 +1,4 @@ +name: 'Configuration override message test' +type: module +package: Testing +version: VERSION diff --git a/core/modules/config/tests/config_override_message_test/config_override_message_test.module b/core/modules/config/tests/config_override_message_test/config_override_message_test.module new file mode 100644 index 0000000000000000000000000000000000000000..4106499fb29323f75b4ab5d5aa23caa313efa12c --- /dev/null +++ b/core/modules/config/tests/config_override_message_test/config_override_message_test.module @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Tests configuration override message functionality. + */ + +use Drupal\Core\Form\FormStateInterface; + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function config_override_message_test_form_system_site_information_settings_alter(array &$form, FormStateInterface $form_state, string $form_id): void { + // Set a weight to a negative amount to ensure the config overrides message + // is above it. + $form['site_information']['#weight'] = -5; +} diff --git a/core/modules/config/tests/src/Functional/ConfigFormOverrideTest.php b/core/modules/config/tests/src/Functional/ConfigFormOverrideTest.php index 5459f18d8b89960fbdacc8e41198c66750fbc044..5b457b99a82d0017e3cb20e1669f3fadb8a42ca6 100644 --- a/core/modules/config/tests/src/Functional/ConfigFormOverrideTest.php +++ b/core/modules/config/tests/src/Functional/ConfigFormOverrideTest.php @@ -14,6 +14,18 @@ */ class ConfigFormOverrideTest extends BrowserTestBase { + /** + * Message text that appears when forms have values for overridden config. + */ + private const OVERRIDE_TEXT = 'These values are overridden. Changes on this form will be saved, but overrides will take precedence. See configuration overrides documentation for more information.'; + + /** + * Modules to enable. + * + * @var array + */ + protected static $modules = ['update', 'config_override_message_test']; + /** * {@inheritdoc} */ @@ -26,31 +38,61 @@ public function testFormsWithOverrides(): void { $this->drupalLogin($this->drupalCreateUser([ 'access administration pages', 'administer site configuration', + 'link to any page', ])); - $overridden_name = 'Site name global conf override'; + // Set up an overrides for configuration that is present in the form. + $settings['config']['system.site']['weight_select_max'] = (object) [ + 'value' => 200, + 'required' => TRUE, + ]; + $this->writeSettings($settings); - // Set up an override. + // Test that although system.site has an overridden key no override + // information is displayed because there is no corresponding form field. + $this->drupalGet('admin/config/system/site-information'); + $this->assertSession()->fieldValueEquals("site_name", 'Drupal'); + $this->assertSession()->pageTextNotContains(self::OVERRIDE_TEXT); + + // Set up an overrides for configuration that is present in the form. + $overridden_name = 'Site name global conf override'; $settings['config']['system.site']['name'] = (object) [ 'value' => $overridden_name, 'required' => TRUE, ]; + $settings['config']['update.settings']['notification']['emails'] = (object) [ + 'value' => [ + 0 => 'a@abc.com', + 1 => 'admin@example.com', + ], + 'required' => TRUE, + ]; $this->writeSettings($settings); - - // Test that everything on the form is the same, but that the override - // worked for the actual site name. $this->drupalGet('admin/config/system/site-information'); $this->assertSession()->titleEquals('Basic site settings | ' . $overridden_name); + $this->assertSession()->elementTextContains('css', 'div[data-drupal-messages]', self::OVERRIDE_TEXT); + // Ensure the configuration overrides message is at the top of the form. + $this->assertSession()->elementExists('css', 'div[data-drupal-messages] + details#edit-site-information'); + $this->assertSession()->elementContains('css', 'div[data-drupal-messages]', '<a href="#edit-site-name" title="\'Site name\' form element">Site name</a>'); $this->assertSession()->fieldValueEquals("site_name", 'Drupal'); - - // Submit the form and ensure the site name is not changed. - $edit = [ + $this->submitForm([ 'site_name' => 'Custom site name', - ]; - $this->drupalGet('admin/config/system/site-information'); - $this->submitForm($edit, 'Save configuration'); + ], 'Save configuration'); $this->assertSession()->titleEquals('Basic site settings | ' . $overridden_name); - $this->assertSession()->fieldValueEquals("site_name", $edit['site_name']); + $this->assertSession()->fieldValueEquals("site_name", 'Custom site name'); + + // Ensure it works for sequence. + $this->drupalGet('admin/reports/updates/settings'); + $this->submitForm([], 'Save configuration'); + $this->assertSession()->pageTextContainsOnce(self::OVERRIDE_TEXT); + // There are two status messages on the page due to the save. + $messages = $this->getSession()->getPage()->findAll('css', 'div[data-drupal-messages]'); + $this->assertCount(2, $messages); + $this->assertStringContainsString('The configuration options have been saved.', $messages[0]->getText()); + $this->assertTrue( + $messages[1]->hasLink('Email addresses to notify when updates are available'), + "Link to 'Email addresses to notify when updates are available' exists" + ); } } diff --git a/core/modules/system/src/Form/SiteInformationForm.php b/core/modules/system/src/Form/SiteInformationForm.php index 372e4e8d2462cd279ae19260902e854dc8ab0141..dbdcdc12d502b75ff12845f99a293616b52e4d5b 100644 --- a/core/modules/system/src/Form/SiteInformationForm.php +++ b/core/modules/system/src/Form/SiteInformationForm.php @@ -5,6 +5,7 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\ConfigTarget; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Path\PathValidatorInterface; use Drupal\Core\Routing\RequestContext; @@ -92,10 +93,6 @@ protected function getEditableConfigNames() { */ public function buildForm(array $form, FormStateInterface $form_state) { $site_config = $this->config('system.site'); - $site_mail = $site_config->get('mail'); - if (empty($site_mail)) { - $site_mail = ini_get('sendmail_from'); - } $form['site_information'] = [ '#type' => 'details', @@ -105,20 +102,24 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['site_information']['site_name'] = [ '#type' => 'textfield', '#title' => $this->t('Site name'), - '#default_value' => $site_config->get('name'), + '#config_target' => 'system.site:name', '#required' => TRUE, ]; $form['site_information']['site_slogan'] = [ '#type' => 'textfield', '#title' => $this->t('Slogan'), - '#default_value' => $site_config->get('slogan'), + '#config_target' => 'system.site:slogan', '#description' => $this->t("How this is used depends on your site's theme."), '#maxlength' => 255, ]; $form['site_information']['site_mail'] = [ '#type' => 'email', '#title' => $this->t('Email address'), - '#default_value' => $site_mail, + '#config_target' => new ConfigTarget( + 'system.site', + 'mail', + fromConfig: fn($value) => $value ?: ini_get('sendmail_from'), + ), '#description' => $this->t("The <em>From</em> address in automated emails sent during registration and new password requests, and other notifications. (Use an address ending in your site's domain to help prevent this email being flagged as spam.)"), '#required' => TRUE, ]; @@ -144,14 +145,14 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['error_page']['site_403'] = [ '#type' => 'textfield', '#title' => $this->t('Default 403 (access denied) page'), - '#default_value' => $site_config->get('page.403'), + '#config_target' => 'system.site:page.403', '#size' => 40, '#description' => $this->t('This page is displayed when the requested document is denied to the current user. Leave blank to display a generic "access denied" page.'), ]; $form['error_page']['site_404'] = [ '#type' => 'textfield', '#title' => $this->t('Default 404 (not found) page'), - '#default_value' => $site_config->get('page.404'), + '#config_target' => 'system.site:page.404', '#size' => 40, '#description' => $this->t('This page is displayed when no other content matches the requested document. Leave blank to display a generic "page not found" page.'), ]; @@ -203,12 +204,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->config('system.site') - ->set('name', $form_state->getValue('site_name')) - ->set('mail', $form_state->getValue('site_mail')) - ->set('slogan', $form_state->getValue('site_slogan')) ->set('page.front', $form_state->getValue('site_frontpage')) - ->set('page.403', $form_state->getValue('site_403')) - ->set('page.404', $form_state->getValue('site_404')) ->save(); parent::submitForm($form, $form_state); diff --git a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml index 404458e422ecf9289e20394fa3541fe6a638d4ab..3267711e7be659a3538e52d6804e12ecdc27ea43 100644 --- a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml +++ b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml @@ -165,7 +165,7 @@ router_test.25: router_test.26: path: '/router_test/test26' defaults: - _form: '\Drupal\system\Form\LoggingForm' + _form: '\Drupal\router_test\Form' _title: 'Cron' requirements: _access: 'TRUE' diff --git a/core/modules/system/tests/modules/router_test_directory/src/Form.php b/core/modules/system/tests/modules/router_test_directory/src/Form.php new file mode 100644 index 0000000000000000000000000000000000000000..33dd872be17802b5ee2b8ff4791d54fa20427cdf --- /dev/null +++ b/core/modules/system/tests/modules/router_test_directory/src/Form.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\router_test; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Form to test _form routing. + * + * @internal + */ +class Form extends FormBase { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'router_test_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + $form['submit'] = [ + '#type' => 'submit', + '#value' => 'Save', + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->messenger()->addStatus('The router_test_form form has been submitted successfully.'); + } + +} diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php index 718af2449d3867cde89f2e1375c494576dcedfa8..71deeb74f821e9cf1966e503d085be58fae1ae98 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php @@ -8,9 +8,9 @@ use Drupal\Core\Form\FormState; use Drupal\Core\Session\AnonymousUserSession; use Drupal\entity_test\Entity\EntityTestMulRevPub; +use Drupal\form_test\Form\FormTestAlterForm; use Drupal\KernelTests\KernelTestBase; use Drupal\language\Entity\ConfigurableLanguage; -use Drupal\system\Form\SiteInformationForm; use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\node\Traits\NodeCreationTrait; @@ -71,6 +71,7 @@ class WorkspaceIntegrationTest extends KernelTestBase { 'language', 'content_translation', 'path_alias', + 'form_test', ]; /** @@ -1073,7 +1074,7 @@ public function testFormCacheForRegularForms(): void { $form_builder = $this->container->get('form_builder'); $form_state = new FormState(); - $built_form = $form_builder->getForm(SiteInformationForm::class, $form_state); + $built_form = $form_builder->getForm(FormTestAlterForm::class, $form_state); $form_builder->setCache($built_form['#build_id'], $built_form, $form_state); } diff --git a/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php b/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php index f9eed1d6eaf361782ed3fb4952ccf8ed60265d46..1c2944a18ed73b71da6231dadf8938ebd961a754 100644 --- a/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php +++ b/core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php @@ -132,7 +132,7 @@ public function testExceptionResponseGeneratedForOriginalRequest(): void { // Test with 404 path pointing to a route that uses '_form'. $response = $this->doTest404Route('/router_test/test26'); - $this->assertStringContainsString('<form class="system-logging-settings"', $response->getContent()); + $this->assertStringContainsString('<form class="router-test-form"', $response->getContent()); // Test with 404 path pointing to a route that uses '_entity_form'. $response = $this->doTest404Route('/router_test/test27');