diff --git a/core/modules/block/js/block.js b/core/modules/block/js/block.js index 1a9566facbdd53dd53accb570b2c5effdc011bae..5224f0a281ef34df8b87acbddfbd89f189b4fa98 100644 --- a/core/modules/block/js/block.js +++ b/core/modules/block/js/block.js @@ -46,7 +46,7 @@ } $( - '[data-drupal-selector="edit-visibility-node-type"], [data-drupal-selector="edit-visibility-entity-bundlenode"], [data-drupal-selector="edit-visibility-language"], [data-drupal-selector="edit-visibility-user-role"]', + '[data-drupal-selector="edit-visibility-node-type"], [data-drupal-selector="edit-visibility-entity-bundlenode"], [data-drupal-selector="edit-visibility-language"], [data-drupal-selector="edit-visibility-user-role"], [data-drupal-selector="edit-visibility-response-status"]', ).drupalSetSummary(checkboxesSummary); $( diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php index 90cd6823ffb4881561f2d1e4fd7136f7245b19f9..4d27062930cfef4be38d871d9d0bb2d1b848342f 100644 --- a/core/modules/block/src/BlockForm.php +++ b/core/modules/block/src/BlockForm.php @@ -247,16 +247,23 @@ protected function buildVisibilityInterface(array $form, FormStateInterface $for $form[$condition_id] = $condition_form; } - if (isset($form['entity_bundle:node'])) { - $form['entity_bundle:node']['negate']['#type'] = 'value'; - $form['entity_bundle:node']['negate']['#title_display'] = 'invisible'; - $form['entity_bundle:node']['negate']['#value'] = $form['entity_bundle:node']['negate']['#default_value']; + // Disable negation for specific conditions. + $disable_negation = [ + 'entity_bundle:node', + 'language', + 'response_status', + 'user_role', + ]; + foreach ($disable_negation as $condition) { + if (isset($form[$condition])) { + $form[$condition]['negate']['#type'] = 'value'; + $form[$condition]['negate']['#value'] = $form[$condition]['negate']['#default_value']; + } } + if (isset($form['user_role'])) { $form['user_role']['#title'] = $this->t('Roles'); unset($form['user_role']['roles']['#description']); - $form['user_role']['negate']['#type'] = 'value'; - $form['user_role']['negate']['#value'] = $form['user_role']['negate']['#default_value']; } if (isset($form['request_path'])) { $form['request_path']['#title'] = $this->t('Pages'); @@ -268,10 +275,6 @@ protected function buildVisibilityInterface(array $form, FormStateInterface $for $this->t('Hide for the listed pages'), ]; } - if (isset($form['language'])) { - $form['language']['negate']['#type'] = 'value'; - $form['language']['negate']['#value'] = $form['language']['negate']['#default_value']; - } return $form; } diff --git a/core/modules/block/tests/src/Functional/BlockTest.php b/core/modules/block/tests/src/Functional/BlockTest.php index 1b31a09e3863b79ee0346ad23bc96f6298aecbc4..4ea7a7534d3db20149a72ce128cc68017291cca4 100644 --- a/core/modules/block/tests/src/Functional/BlockTest.php +++ b/core/modules/block/tests/src/Functional/BlockTest.php @@ -35,11 +35,13 @@ public function testBlockVisibility() { 'settings[label]' => $title, 'settings[label_display]' => TRUE, ]; - // Set the block to be hidden on any user path, and to be shown only to - // authenticated users. + // Set the block to be hidden on any user path, to be shown only to + // authenticated users, and to be shown only on 200 and 404 responses. $edit['visibility[request_path][pages]'] = '/user*'; $edit['visibility[request_path][negate]'] = TRUE; $edit['visibility[user_role][roles][' . RoleInterface::AUTHENTICATED_ID . ']'] = TRUE; + $edit['visibility[response_status][status_codes][200]'] = 200; + $edit['visibility[response_status][status_codes][404]'] = 404; $this->drupalGet('admin/structure/block/add/' . $block_name . '/' . $default_theme); $this->assertSession()->checkboxChecked('edit-visibility-request-path-negate-0'); @@ -48,16 +50,26 @@ public function testBlockVisibility() { $this->clickLink('Configure'); $this->assertSession()->checkboxChecked('edit-visibility-request-path-negate-1'); + $this->assertSession()->checkboxChecked('edit-visibility-response-status-status-codes-200'); + $this->assertSession()->checkboxChecked('edit-visibility-response-status-status-codes-404'); - // Confirm that the block is displayed on the front page. + // Confirm that the block is displayed on the front page (200 response). $this->drupalGet(''); $this->assertSession()->pageTextContains($title); - // Confirm that the block is not displayed according to block visibility + // Confirm that the block is not displayed according to path visibility // rules. $this->drupalGet('user'); $this->assertSession()->pageTextNotContains($title); + // Confirm that the block is displayed on a 404 response. + $this->drupalGet('/0/null'); + $this->assertSession()->pageTextContains($title); + + // Confirm that the block is not displayed on a 403 response. + $this->drupalGet('/admin/config/system/cron'); + $this->assertSession()->pageTextNotContains($title); + // Confirm that the block is not displayed to anonymous users. $this->drupalLogout(); $this->drupalGet(''); diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index e6a5260d2b6e7327eb4e74129653542350407f81..1d8c72c76d973a0996e9510eedc226ea6de71167 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -357,6 +357,14 @@ condition.plugin.request_path: pages: type: string +condition.plugin.response_status: + type: condition.plugin + mapping: + status_codes: + type: sequence + sequence: + type: integer + system.feature_flags: type: config_object label: 'System Feature Flags' diff --git a/core/modules/system/src/Plugin/Condition/ResponseStatus.php b/core/modules/system/src/Plugin/Condition/ResponseStatus.php new file mode 100644 index 0000000000000000000000000000000000000000..aa82db66b99e1e50f25574ce72fb3df8ad973989 --- /dev/null +++ b/core/modules/system/src/Plugin/Condition/ResponseStatus.php @@ -0,0 +1,124 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\system\Plugin\Condition; + +use Drupal\Core\Condition\ConditionPluginBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\PluralTranslatableMarkup; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; + +/** + * Provides a 'Response status' condition. + * + * @Condition( + * id = "response_status", + * label = @Translation("Response status"), + * ) + */ +class ResponseStatus extends ConditionPluginBase implements ContainerFactoryPluginInterface { + + /** + * The request stack. + */ + protected RequestStack $requestStack; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + $instance = new static($configuration, $plugin_id, $plugin_definition); + $instance->setRequestStack($container->get('request_stack')); + return $instance; + } + + public function setRequestStack(RequestStack $requestStack): void { + $this->requestStack = $requestStack; + } + + /** + * {@inheritdoc} + */ + public function isNegated(): bool { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration(): array { + return ['status_codes' => []] + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { + $status_codes = [ + Response::HTTP_OK => $this->t('Success (@status_code)', ['@status_code' => Response::HTTP_OK]), + Response::HTTP_FORBIDDEN => $this->t('Access denied (@status_code)', ['@status_code' => Response::HTTP_FORBIDDEN]), + Response::HTTP_NOT_FOUND => $this->t('Page not found (@status_code)', ['@status_code' => Response::HTTP_NOT_FOUND]), + ]; + $form['status_codes'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Response status'), + '#options' => $status_codes, + '#default_value' => $this->configuration['status_codes'], + '#description' => $this->t('Shows the block on pages with any matching response status. If nothing is checked, the block is shown on all pages. Other response statuses are not used.'), + ]; + return parent::buildConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void { + $this->configuration['status_codes'] = array_keys(array_filter($form_state->getValue('status_codes'))); + parent::submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function summary(): PluralTranslatableMarkup { + $allowed_codes = $this->configuration['status_codes']; + $status_codes = [Response::HTTP_OK, Response::HTTP_FORBIDDEN, Response::HTTP_NOT_FOUND]; + $result = empty($allowed_codes) ? $status_codes : $allowed_codes; + $count = count($result); + $codes = implode(', ', $result); + if (!empty($this->configuration['negate'])) { + return $this->formatPlural($count, 'Request response code is not: @codes', 'Request response code is not one of the following: @codes', ['@codes' => $codes]); + } + return $this->formatPlural($count, 'Request response code is: @codes', 'Request response code is one of the following: @codes', ['@codes' => $codes]); + } + + /** + * {@inheritdoc} + */ + public function evaluate(): bool { + $allowed_codes = $this->configuration['status_codes']; + if (empty($allowed_codes)) { + return TRUE; + } + $exception = $this->requestStack->getCurrentRequest()->attributes->get('exception'); + if ($exception) { + return ($exception instanceof HttpExceptionInterface && in_array($exception->getStatusCode(), $allowed_codes, TRUE)); + } + return in_array(Response::HTTP_OK, $allowed_codes, TRUE); + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts(): array { + $contexts = parent::getCacheContexts(); + $contexts[] = 'url.path'; + return $contexts; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Plugin/Condition/ResponseStatusTest.php b/core/tests/Drupal/KernelTests/Core/Plugin/Condition/ResponseStatusTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9f5690a6bf3ae6b5e007c1a8eac4852ca77128de --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Plugin/Condition/ResponseStatusTest.php @@ -0,0 +1,287 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Plugin\Condition; + +use Drupal\Core\Condition\ConditionManager; +use Drupal\KernelTests\KernelTestBase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\HttpException; + +/** + * Tests the Response Status Condition, provided by the system module. + * + * @group Plugin + */ +class ResponseStatusTest extends KernelTestBase { + + /** + * The condition plugin manager under test. + */ + protected ConditionManager $pluginManager; + + /** + * The request stack used for testing. + */ + protected RequestStack $requestStack; + + /** + * {@inheritdoc} + */ + protected static $modules = ['system', 'user']; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->installConfig('system'); + + $this->pluginManager = $this->container->get('plugin.manager.condition'); + + // Set the test request stack in the container. + $this->requestStack = new RequestStack(); + $this->container->set('request_stack', $this->requestStack); + } + + /** + * Tests the request path condition. + * + * @dataProvider providerTestConditions + */ + public function testConditions(array $status_codes, bool $negate, int $response_code, bool $expected_execute) { + if ($response_code === Response::HTTP_OK) { + $request = Request::create('/my/valid/page'); + } + else { + $request = new Request(); + $request->attributes->set('exception', new HttpException($response_code)); + } + $this->requestStack->push($request); + + /** @var \Drupal\system\Plugin\Condition\ResponseStatus $condition */ + $condition = $this->pluginManager->createInstance('response_status'); + $condition->setConfig('status_codes', $status_codes); + $condition->setConfig('negate', $negate); + + $this->assertSame($expected_execute, $condition->execute()); + } + + /** + * Provides test data for testConditions. + */ + public function providerTestConditions() { + // Default values with 200 response code. + yield [ + 'status_codes' => [], + 'negate' => FALSE, + 'response_code' => Response::HTTP_OK, + 'expected_execute' => TRUE, + ]; + + // Default values with 403 response code. + yield [ + 'status_codes' => [], + 'negate' => FALSE, + 'response_code' => Response::HTTP_FORBIDDEN, + 'expected_execute' => TRUE, + ]; + + // Default values with 404 response code. + yield [ + 'status_codes' => [], + 'negate' => FALSE, + 'response_code' => Response::HTTP_NOT_FOUND, + 'expected_execute' => TRUE, + ]; + + // 200 status code enabled with 200 response code. + yield [ + 'status_codes' => [Response::HTTP_OK => Response::HTTP_OK], + 'negate' => FALSE, + 'response_code' => Response::HTTP_OK, + 'expected_execute' => TRUE, + ]; + + // 200 status code enabled with 403 response code. + yield [ + 'status_codes' => [Response::HTTP_OK => Response::HTTP_OK], + 'negate' => FALSE, + 'response_code' => Response::HTTP_FORBIDDEN, + 'expected_execute' => FALSE, + ]; + + // 200 status code enabled with 404 response code. + yield [ + 'status_codes' => [Response::HTTP_OK => Response::HTTP_OK], + 'negate' => FALSE, + 'response_code' => Response::HTTP_NOT_FOUND, + 'expected_execute' => FALSE, + ]; + + // 403 status code enabled with 200 response code. + yield [ + 'status_codes' => [Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN], + 'negate' => FALSE, + 'response_code' => Response::HTTP_OK, + 'expected_execute' => FALSE, + ]; + + // 403 status code enabled with 403 response code. + yield [ + 'status_codes' => [Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN], + 'negate' => FALSE, + 'response_code' => Response::HTTP_FORBIDDEN, + 'expected_execute' => TRUE, + ]; + + // 403 status code enabled with 404 response code. + yield [ + 'status_codes' => [Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN], + 'negate' => FALSE, + 'response_code' => Response::HTTP_NOT_FOUND, + 'expected_execute' => FALSE, + ]; + + // 200,403 status code enabled with 200 response code. + yield [ + 'status_codes' => [ + Response::HTTP_OK => Response::HTTP_OK, + Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_OK, + 'expected_execute' => TRUE, + ]; + + // 200,403 status code enabled with 403 response code. + yield [ + 'status_codes' => [ + Response::HTTP_OK => Response::HTTP_OK, + Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_FORBIDDEN, + 'expected_execute' => TRUE, + ]; + + // 200,403 status code enabled with 404 response code. + yield [ + 'status_codes' => [ + Response::HTTP_OK => Response::HTTP_OK, + Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_NOT_FOUND, + 'expected_execute' => FALSE, + ]; + + // 200,404 status code enabled with 200 response code. + yield [ + 'status_codes' => [ + Response::HTTP_OK => Response::HTTP_OK, + Response::HTTP_NOT_FOUND => Response::HTTP_NOT_FOUND, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_OK, + 'expected_execute' => TRUE, + ]; + + // 200,404 status code enabled with 403 response code. + yield [ + 'status_codes' => [ + Response::HTTP_OK => Response::HTTP_OK, + Response::HTTP_NOT_FOUND => Response::HTTP_NOT_FOUND, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_FORBIDDEN, + 'expected_execute' => FALSE, + ]; + + // 200,404 status code enabled with 404 response code. + yield [ + 'status_codes' => [ + Response::HTTP_OK => Response::HTTP_OK, + Response::HTTP_NOT_FOUND => Response::HTTP_NOT_FOUND, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_NOT_FOUND, + 'expected_execute' => TRUE, + ]; + + // 403,404 status code enabled with 200 response code. + yield [ + 'status_codes' => [ + Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN, + Response::HTTP_NOT_FOUND => Response::HTTP_NOT_FOUND, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_OK, + 'expected_execute' => FALSE, + ]; + + // 403,404 status code enabled with 403 response code. + yield [ + 'status_codes' => [ + Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN, + Response::HTTP_NOT_FOUND => Response::HTTP_NOT_FOUND, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_FORBIDDEN, + 'expected_execute' => TRUE, + ]; + + // 403,404 status code enabled with 404 response code. + yield [ + 'status_codes' => [ + Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN, + Response::HTTP_NOT_FOUND => Response::HTTP_NOT_FOUND, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_NOT_FOUND, + 'expected_execute' => TRUE, + ]; + + // 200,403,404 status code enabled with 200 response code. + yield [ + 'status_codes' => [ + Response::HTTP_OK => Response::HTTP_OK, + Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN, + Response::HTTP_NOT_FOUND => Response::HTTP_NOT_FOUND, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_OK, + 'expected_execute' => TRUE, + ]; + + // 200,403 status code enabled with 403 response code. + yield [ + 'status_codes' => [ + Response::HTTP_OK => Response::HTTP_OK, + Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN, + Response::HTTP_NOT_FOUND => Response::HTTP_NOT_FOUND, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_FORBIDDEN, + 'expected_execute' => TRUE, + ]; + + // 200,403 status code enabled with 404 response code. + yield [ + 'status_codes' => [ + Response::HTTP_OK => Response::HTTP_OK, + Response::HTTP_FORBIDDEN => Response::HTTP_FORBIDDEN, + Response::HTTP_NOT_FOUND => Response::HTTP_NOT_FOUND, + ], + 'negate' => FALSE, + 'response_code' => Response::HTTP_NOT_FOUND, + 'expected_execute' => TRUE, + ]; + + } + +}