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