From badd417e8c5648e397b3a8f7a6985c8bcfd57b96 Mon Sep 17 00:00:00 2001 From: Lauri Eskola <lauri.eskola@acquia.com> Date: Sat, 29 Jul 2023 15:05:26 +0300 Subject: [PATCH] =?UTF-8?q?Issue=20#2245767=20by=20John=20Pitcairn,=20andr?= =?UTF-8?q?ewmacpherson,=20danflanagan8,=20leymannx,=20tim.plunkett,=20mpo?= =?UTF-8?q?lishchuck,=20tobiasb,=20olli,=20dawehner,=20Anas=5Fmaw,=20SpadX?= =?UTF-8?q?III,=20=5Futsavsharma,=20benjifisher,=20ankithashetty,=20jidron?= =?UTF-8?q?e,=20SimeonKesmev,=20Daniel=20Korte,=20ameymudras,=20jyotimishr?= =?UTF-8?q?a-developer,=20alexpott,=20smustgrave,=20geek-merlin,=20catch,?= =?UTF-8?q?=20DuaelFr,=20G=C3=A1bor=20Hojtsy,=20lauriii,=20ckrina,=20quiet?= =?UTF-8?q?one:=20Allow=20blocks=20to=20be=20configured=20to=20show/hide?= =?UTF-8?q?=20on=20200/403/404=20response=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/modules/block/js/block.js | 2 +- core/modules/block/src/BlockForm.php | 23 +- .../block/tests/src/Functional/BlockTest.php | 20 +- .../system/config/schema/system.schema.yml | 8 + .../src/Plugin/Condition/ResponseStatus.php | 124 ++++++++ .../Plugin/Condition/ResponseStatusTest.php | 287 ++++++++++++++++++ 6 files changed, 449 insertions(+), 15 deletions(-) create mode 100644 core/modules/system/src/Plugin/Condition/ResponseStatus.php create mode 100644 core/tests/Drupal/KernelTests/Core/Plugin/Condition/ResponseStatusTest.php diff --git a/core/modules/block/js/block.js b/core/modules/block/js/block.js index 1a9566facbdd..5224f0a281ef 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 90cd6823ffb4..4d27062930cf 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 1b31a09e3863..4ea7a7534d3d 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 e6a5260d2b6e..1d8c72c76d97 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 000000000000..aa82db66b99e --- /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 000000000000..9f5690a6bf3a --- /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, + ]; + + } + +} -- GitLab