From 3b0dacd94476c5b00be76d848e8ca94909451acd Mon Sep 17 00:00:00 2001
From: "Theresa.Grannum" <theresa.grannum@3688861.no-reply.drupal.org>
Date: Wed, 5 Oct 2022 21:03:35 +0000
Subject: [PATCH] Issue #3312669 by phenaproxima, Theresa.Grannum: Add help
 text explaining how to set an alternate port for cron

---
 automatic_updates.module                      |  5 +++
 automatic_updates.services.yml                |  1 +
 src/Validator/CronServerValidator.php         | 35 +++++++++++++----
 .../CronServerValidatorTest.php               | 38 ++++++++++++++++++-
 4 files changed, 71 insertions(+), 8 deletions(-)

diff --git a/automatic_updates.module b/automatic_updates.module
index e793fd50cf..d3fe134f8d 100644
--- a/automatic_updates.module
+++ b/automatic_updates.module
@@ -30,6 +30,11 @@ function automatic_updates_help($route_name, RouteMatchInterface $route_match) {
       $output .= '<h3>' . t('Requirements') . '</h3>';
       $output .= '<p>' . t('Automatic Updates requires a Composer executable whose version satisfies <code>@version</code>, and PHP must have permission to run it. The path to the executable may be set in the <code>package_manager.settings:executables.composer</code> config setting, or it will be automatically detected.', ['@version' => ComposerExecutableValidator::MINIMUM_COMPOSER_VERSION_CONSTRAINT]) . '</p>';
       $output .= '<p>' . t('For more information, see the <a href=":automatic-updates-documentation">online documentation for the Automatic Updates module</a>.', [':automatic-updates-documentation' => 'https://www.drupal.org/docs/8/update/automatic-updates']) . '</p>';
+      $output .= '<p id="cron-alternate-port">' . t('If your site is running on the built-in PHP web server, unattended (i.e., cron) updates may not work without one of the following workarounds:') . '</p>';
+      $output .= '<ul>';
+      $output .= '<li>' . t('Use a multithreaded web server, such as Apache, NGINX, or on Windows, IIS.') . '</li>';
+      $output .= '<li>' . t('Run another instance of the built-in PHP web server on a different port and configure automatic updates accordingly: <code>$config["automatic_updates.settings"]["cron_port"] = $alternate_port_number;</code>') . '</li>';
+      $output .= '</ul>';
       return $output;
   }
 }
diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index e78d85265d..00eb7f862e 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -166,6 +166,7 @@ services:
     arguments:
       - '@request_stack'
       - '@config.factory'
+      - '@module_handler'
     tags:
       - { name: event_subscriber }
   logger.channel.automatic_updates:
diff --git a/src/Validator/CronServerValidator.php b/src/Validator/CronServerValidator.php
index 5c4b9cbdcc..5d2ce9fc46 100644
--- a/src/Validator/CronServerValidator.php
+++ b/src/Validator/CronServerValidator.php
@@ -5,8 +5,10 @@ namespace Drupal\automatic_updates\Validator;
 use Drupal\automatic_updates\CronUpdater;
 use Drupal\automatic_updates\Event\ReadinessCheckEvent;
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Http\RequestStack;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\Event\PreOperationStageEvent;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -37,6 +39,13 @@ final class CronServerValidator implements EventSubscriberInterface {
    */
   protected $configFactory;
 
+  /**
+   * The module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
   /**
    * The type of interface between the web server and the PHP runtime.
    *
@@ -54,10 +63,13 @@ final class CronServerValidator implements EventSubscriberInterface {
    *   The request stack service.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config factory service.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
    */
-  public function __construct(RequestStack $request_stack, ConfigFactoryInterface $config_factory) {
+  public function __construct(RequestStack $request_stack, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler) {
     $this->request = $request_stack->getCurrentRequest();
     $this->configFactory = $config_factory;
+    $this->moduleHandler = $module_handler;
   }
 
   /**
@@ -79,13 +91,22 @@ final class CronServerValidator implements EventSubscriberInterface {
     $alternate_port = intval($alternate_port) ?: $current_port;
 
     if (static::$serverApi === 'cli-server' && $current_port === $alternate_port) {
-      // @todo Explain how to fix this problem on our help page, and link to it,
-      //   in https://drupal.org/i/3312669.
-      $event->addError([
-        $this->t('Your site appears to be running on the built-in PHP web server on port @port. Drupal cannot be automatically updated with this configuration unless the site can also be reached on an alternate port.', [
-          '@port' => $current_port,
-        ]),
+      $message = $this->t('Your site appears to be running on the built-in PHP web server on port @port. Drupal cannot be automatically updated with this configuration unless the site can also be reached on an alternate port.', [
+        '@port' => $current_port,
       ]);
+      if ($this->moduleHandler->moduleExists('help')) {
+        $url = Url::fromRoute('help.page')
+          ->setRouteParameter('name', 'automatic_updates')
+          ->setOption('fragment', 'cron-alternate-port')
+          ->toString();
+
+        $message = $this->t('@message See <a href=":url">the Automatic Updates help page</a> for more information on how to resolve this.', [
+          '@message' => $message,
+          ':url' => $url,
+        ]);
+      }
+
+      $event->addError([$message]);
     }
   }
 
diff --git a/tests/src/Kernel/ReadinessValidation/CronServerValidatorTest.php b/tests/src/Kernel/ReadinessValidation/CronServerValidatorTest.php
index 5c13b4de05..ab0fa728e7 100644
--- a/tests/src/Kernel/ReadinessValidation/CronServerValidatorTest.php
+++ b/tests/src/Kernel/ReadinessValidation/CronServerValidatorTest.php
@@ -5,6 +5,7 @@ namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation;
 use Drupal\automatic_updates\CronUpdater;
 use Drupal\automatic_updates\Validator\CronServerValidator;
 use Drupal\Core\Logger\RfcLogLevel;
+use Drupal\Core\Url;
 use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
@@ -68,7 +69,7 @@ class CronServerValidatorTest extends AutomaticUpdatesKernelTestBase {
   }
 
   /**
-   * Tests server configuration validation for unattended updates.
+   * Tests server validation for unattended updates.
    *
    * @param bool $alternate_port
    *   Whether or not an alternate port should be set.
@@ -118,4 +119,39 @@ class CronServerValidatorTest extends AutomaticUpdatesKernelTestBase {
     }
   }
 
+  /**
+   * Tests server validation for unattended updates with Help enabled.
+   *
+   * @param bool $alternate_port
+   *   Whether or not an alternate port should be set.
+   * @param string $server_api
+   *   The value of the PHP_SAPI constant, as known to the validator.
+   * @param string[] $cron_modes
+   *   The cron modes to test with. Can contain any of
+   *   \Drupal\automatic_updates\CronUpdater::DISABLED,
+   *   \Drupal\automatic_updates\CronUpdater::SECURITY, and
+   *   \Drupal\automatic_updates\CronUpdater::ALL.
+   * @param \Drupal\package_manager\ValidationResult[] $expected_results
+   *   The expected validation results.
+   *
+   * @dataProvider providerCronServerValidation
+   */
+  public function testHelpLink(bool $alternate_port, string $server_api, array $cron_modes, array $expected_results): void {
+    $this->enableModules(['help']);
+
+    $url = Url::fromRoute('help.page')
+      ->setRouteParameter('name', 'automatic_updates')
+      ->setOption('fragment', 'cron-alternate-port')
+      ->toString();
+
+    foreach ($expected_results as $i => $result) {
+      $messages = [];
+      foreach ($result->getMessages() as $message) {
+        $messages[] = "$message See <a href=\"$url\">the Automatic Updates help page</a> for more information on how to resolve this.";
+      }
+      $expected_results[$i] = ValidationResult::createError($messages);
+    }
+    $this->testCronServerValidation($alternate_port, $server_api, $cron_modes, $expected_results);
+  }
+
 }
-- 
GitLab