From 57260445dafa53e317790c732b8b9f06d33d7507 Mon Sep 17 00:00:00 2001
From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org>
Date: Thu, 6 Feb 2025 21:57:27 +0000
Subject: [PATCH] Issue #3494512: Package Manager validation is only necessary
 if the project to install is not already in the filesystem

---
 src/Controller/InstallerController.php        |  43 ++++++++++++
 src/Element/ProjectBrowser.php                |  22 +-----
 src/InstallReadiness.php                      |  63 ------------------
 src/ProjectBrowserServiceProvider.php         |   3 -
 sveltejs/public/build/bundle.js               | Bin 299622 -> 298920 bytes
 sveltejs/public/build/bundle.js.map           | Bin 270987 -> 269655 bytes
 sveltejs/src/DetailModal.svelte               |   2 +-
 sveltejs/src/Project/ActionButton.svelte      |   2 +-
 sveltejs/src/ProjectBrowser.svelte            |  22 ------
 sveltejs/src/ProjectGrid.svelte               |   2 +-
 .../src/ProjectBrowserTestServiceProvider.php |   6 +-
 .../ProjectBrowserInstallerUiTest.php         |  16 +++--
 12 files changed, 60 insertions(+), 121 deletions(-)
 delete mode 100644 src/InstallReadiness.php

diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php
index 0be6732ac..f91671d3f 100644
--- a/src/Controller/InstallerController.php
+++ b/src/Controller/InstallerController.php
@@ -8,24 +8,29 @@ use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Url;
 use Drupal\package_manager\Exception\StageException;
+use Drupal\package_manager\StatusCheckTrait;
 use Drupal\project_browser\ActivatorInterface;
 use Drupal\project_browser\ComposerInstaller\Installer;
 use Drupal\project_browser\EnabledSourceHandler;
 use Drupal\project_browser\InstallState;
 use Drupal\project_browser\ProjectBrowser\Project;
 use Drupal\project_browser_test\Plugin\ProjectBrowserSource\ProjectBrowserTestMock;
+use Drupal\system\SystemManager;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 
 /**
  * Defines a controller to install projects via UI.
  */
 final class InstallerController extends ControllerBase {
 
+  use StatusCheckTrait;
+
   /**
    * No require or install in progress for a given module.
    *
@@ -61,6 +66,7 @@ final class InstallerController extends ControllerBase {
     private readonly LoggerInterface $logger,
     private readonly ActivatorInterface $activator,
     private readonly InstallState $installState,
+    private readonly EventDispatcherInterface $eventDispatcher,
   ) {}
 
   /**
@@ -78,6 +84,7 @@ final class InstallerController extends ControllerBase {
       $container->get('logger.channel.project_browser'),
       $container->get(ActivatorInterface::class),
       $container->get(InstallState::class),
+      $container->get(EventDispatcherInterface::class),
     );
   }
 
@@ -323,6 +330,16 @@ final class InstallerController extends ControllerBase {
       return $this->lockedResponse($message, $unlock_url);
     }
 
+    // Ensure the environment is ready to use Package Manager.
+    ['errors' => $errors, 'warnings' => $warnings] = $this->validatePackageManager();
+    if ($warnings) {
+      $this->logger->warning(implode("\n", $warnings));
+    }
+    if ($errors) {
+      $error_message = '<ul><li>' . implode('</li><li>', $errors) . '</li></ul>';
+      return $this->errorResponse(new StageException($this->installer, $error_message));
+    }
+
     try {
       $stage_id = $this->installer->create();
     }
@@ -464,4 +481,30 @@ final class InstallerController extends ControllerBase {
     return $response ?? new JsonResponse(['status' => 0]);
   }
 
+  /**
+   * Checks if the environment meets Package Manager install requirements.
+   *
+   * @return array[]
+   *   An array with two sub-elements:
+   *   - errors: The validation messages with an "error" severity.
+   *   - warnings: All other validation messages, which are probably warnings.
+   */
+  private function validatePackageManager(): array {
+    $results = [
+      'errors' => [],
+      'warnings' => [],
+    ];
+    foreach ($this->runStatusCheck($this->installer, $this->eventDispatcher) as $result) {
+      $group = $result->severity === SystemManager::REQUIREMENT_ERROR
+        ? 'errors'
+        : 'warnings';
+
+      if ($result->summary) {
+        $results[$group][] = $result->summary;
+      }
+      $results[$group] = array_merge($results[$group], $result->messages);
+    }
+    return $results;
+  }
+
 }
diff --git a/src/Element/ProjectBrowser.php b/src/Element/ProjectBrowser.php
index 00d0dee06..254d54761 100644
--- a/src/Element/ProjectBrowser.php
+++ b/src/Element/ProjectBrowser.php
@@ -13,7 +13,6 @@ use Drupal\Core\Render\Attribute\RenderElement;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\Element\ElementInterface;
 use Drupal\Core\Render\Element\RenderElementBase;
-use Drupal\project_browser\InstallReadiness;
 use Drupal\project_browser\Plugin\ProjectBrowserSourceInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -30,7 +29,6 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn
   public function __construct(
     private readonly string $pluginId,
     private readonly mixed $pluginDefinition,
-    private readonly ?InstallReadiness $installReadiness,
     private readonly ModuleHandlerInterface $moduleHandler,
     private readonly ConfigFactoryInterface $configFactory,
     private readonly UuidInterface $uuid,
@@ -55,13 +53,9 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
-    $install_readiness = $container->get(InstallReadiness::class, ContainerInterface::NULL_ON_INVALID_REFERENCE);
-    assert(is_null($install_readiness) || $install_readiness instanceof InstallReadiness);
-
     return new static(
       $plugin_id,
       $plugin_definition,
-      $install_readiness,
       $container->get(ModuleHandlerInterface::class),
       $container->get(ConfigFactoryInterface::class),
       $container->get(UuidInterface::class),
@@ -127,25 +121,11 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn
     if (is_int($max_selections) && $max_selections <= 0) {
       throw new \InvalidArgumentException('$max_selections must be a positive integer or NULL.');
     }
-
-    $package_manager = [
-      'available' => $this->configFactory->get('project_browser.admin_settings')->get('allow_ui_install') && is_object($this->installReadiness),
-      'errors' => [],
-      'warnings' => [],
-      'status_checked' => FALSE,
-    ];
-    // @todo Fix https://www.drupal.org/node/3494512 to avoid adding
-    // hard-coded values. #techdebt
-    if ($source->getPluginId() !== 'recipes' && $package_manager['available'] && is_object($this->installReadiness)) {
-      $package_manager = array_merge($package_manager, $this->installReadiness->validatePackageManager());
-      $package_manager['status_checked'] = TRUE;
-    }
-
     $sort_options = $source->getSortOptions();
     return [
       'module_path' => $this->moduleHandler->getModule('project_browser')->getPath(),
       'default_plugin_id' => $source->getPluginId(),
-      'package_manager' => $package_manager,
+      'package_manager' => $this->configFactory->get('project_browser.admin_settings')->get('allow_ui_install') && $this->moduleHandler->moduleExists('package_manager'),
       'max_selections' => $max_selections,
       'current_path' => '/' . $this->currentPath->getPath(),
       'instances' => [
diff --git a/src/InstallReadiness.php b/src/InstallReadiness.php
deleted file mode 100644
index c3e6ae040..000000000
--- a/src/InstallReadiness.php
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-namespace Drupal\project_browser;
-
-use Drupal\package_manager\StatusCheckTrait;
-use Drupal\project_browser\ComposerInstaller\Installer;
-use Drupal\system\SystemManager;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
-
-/**
- * Defines Installer service.
- */
-class InstallReadiness {
-
-  use StatusCheckTrait;
-
-  public function __construct(
-    private readonly Installer $installer,
-    private readonly EventDispatcherInterface $eventDispatcher,
-  ) {}
-
-  /**
-   * Checks if the environment meets Package Manager install requirements.
-   *
-   * @return array[]
-   *   errors - an array of messages with severity 2
-   *   messages - all other messages below severity 2 (warnings)
-   */
-  public function validatePackageManager() {
-    $errors = [];
-    $warnings = [];
-    foreach ($this->runStatusCheck($this->installer, $this->eventDispatcher) as $result) {
-      $messages = $result->messages;
-      $summary = $result->summary;
-      if ($summary) {
-        array_unshift($messages, $summary);
-      }
-      $text = implode("\n", $messages);
-
-      if ($result->severity === SystemManager::REQUIREMENT_ERROR) {
-        $errors[] = $text;
-      }
-      else {
-        $warnings[] = $text;
-      }
-    }
-    return [
-      'errors' => $errors,
-      'warnings' => $warnings,
-    ];
-  }
-
-  /**
-   * Checks if the installer is available.
-   *
-   * @return bool
-   *   If the installer is currently available.
-   */
-  public function installerAvailable() {
-    return $this->installer->isAvailable();
-  }
-
-}
diff --git a/src/ProjectBrowserServiceProvider.php b/src/ProjectBrowserServiceProvider.php
index 595a6a4d9..1dcd23e1d 100644
--- a/src/ProjectBrowserServiceProvider.php
+++ b/src/ProjectBrowserServiceProvider.php
@@ -41,9 +41,6 @@ class ProjectBrowserServiceProvider extends ServiceProviderBase {
         ->setArgument('$keyValueFactory', new Reference('keyvalue'))
         ->setAutowired(TRUE);
 
-      $container->register(InstallReadiness::class, InstallReadiness::class)
-        ->setAutowired(TRUE);
-
       $container->register(CoreNotUpdatedValidator::class, CoreNotUpdatedValidator::class)
         ->addTag('event_subscriber')
         ->setAutowired(TRUE);
diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index 8027f477d5716190e8c5bc1eaeb350d90a124a48..252132cb62651d7b2b0f2dfc5fdd969dafb7d84e 100644
GIT binary patch
delta 185
zcmaF1L}<l)p@uDtcXn@owwsZicl-Afj57{wKhVi^SaSNrT1NTpmlreLTmoiJ&(C4z
z*nZc8xkP#T#vVqI?Lhtwq3wb7%mNFU49vGDE3mv`VKOk=?yt%s#Li@5usvCag@u*L
z)NFgEA<IuDCR5YxDJCp8nVHPYw%c2<nDDZh7+RTVDokd4D!RSTl|_=5%@84QD2U}Z
e3zM<s_Wj{3jIz`BN;9!fwtgV9owuANQW^lRRzDB`

delta 835
zcmZ26U+CEqp@uDtcXo5>C6*;-<|HQNq)r!D%A~Sw594DVK7^oxnwrA&g-e*kxBolA
zSbM+_RVKjE+1t_GHQv|J4@d>+r4|+C7ZvN}q~@iUWGL9$+A0`KKe&WRV(Lv6!RZZ~
zm^inu?PQuLsZ4|^)8}4f*4-YvnCa#c4w&-k8~K?frhhmBG$+)9xki~CDluI#hgp34
z(`@D&LP-AEzPFB9U?G!{<@WRPEU#FYj4ZbAR$&ohXEHO|eqNh}g_X(NV*6DCmY+;a
z=H}Zk7_;1DX0ou@zS^9{gqO+8c>6;a79(CJW8>*ShU9j=AeP@OOlF4LwZmB$Wv9z>
zG4f1bSI5j>4|7~jDk!Wp@CQjvjRLxY^2DON%)E41sA*~{RC6f+0Vfb-rV%huQ=uNA
zniJh<Xmq6I7r7=TXK17<*xDf(&dHgZT3if_s?;LA#FP|`gwQ-tGEgYVS4hb(&&$bA
zOi{?qPbtkwEmkPYOjIbzNL2{+v{F#1POVMQQK(iZsVqpfnl4ttBErncSv!5BAd^@<
zHW#D&5ZMjrPKK)`*3lsusYR&@xrvnuNvR5%{>8<mK<AccWF}`Qq^6~%CYQiH0}N!C
vf6zUlpboWHU7=Rf8Xo*WPc59uC^}u?0VBtBo3Bj#lTY8{-R@t)5-AM;7P1P1

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index 12e6f84478a3785c493a3b22f5cc6b21035b4c04..da849d5c90149ad2b157ea1e813ac8bb8ebbf0e5 100644
GIT binary patch
delta 232
zcmeA^DsX+5K*JWsUk9eId&#J_UF#rYgZlQ{FBnfMZePd66l&7G+L3AdYDeZIaTZTU
z*MjK@|Cq%DQXL&rb<&+39X)m29UV)ZrU$w(i%fTlWL9CSa+)sK$1JkFCX!j)dio|C
zW)W6T$8zVe?f0iJCzuKsIynaBIO%{5@N{%^bkWJ3o><E)2{hb4ZTkAt%o@}GonjW>
ze&!UjDl1nwT=8`N3(V$>Y10$lF^g@FyTDwa$&?+q{lZUXUtXrH!0qzvEPuG!N}U|t
b^IWDYTw@m9t{}=1$jxNxx4l?_MS=+c;^9tQ

delta 1312
zcmaKqTSyd99LCu>Yp$AUu4ZA{F)zCd!*&rhTr|$k4$GvgrYQ)bj=N{wv31sV*WJ8j
zK1K8pVs(0oeCesc2XU$=2_%Aq5F#)vC8D60?4bt<YA$<m*Ki(YhI792`@Zi#^WkOw
zoz?uCOS;{aY%3q(1wTLN$I|YK?uqGKasgIX!*;|pBPZMMLap+n!-|8!Krm$WV{s_#
zgN==i@T~OsyY5Z-9uS!=pu*B-Hnm+d(<?&t61>nHe~0_1i43x3Qe{5ad}?YRC&~v!
zK^%(bv-IMay#-Dtxk{D;(JAWvX=5|+27)Z__L<Fie&Kmx-v|zc<f-7GmG^ke16`sr
zQy30FParD#1H1>01UzGY9D)&nhhZOvU0egKoW#=u^xkyZyu#z7xB;3}D-+!-&TqXa
znZLc1-+yq8^Z}%T(@Q7Rsa0lDt0Wz@XaiS#IEdj0KMn^m6hfgfxl`09xP1_Nz1SU2
zG?qU_ymY*2I&2cgn#Qu2;51#3Dzez*`ZG&qWcjP^#I5xvN?LCMt;IzFJ7P!eG)4&1
z3_=XuiBKoGdk>r>?{a~j^mT(uok*^^L6P*Z8x-4)Bvdqq5J$E*KoN=dfC>YLqD+m0
zW*@A9Lg~{SxKwN9&}F|};UYzh6gZ-t$}QDU-F1*z0hEHa2|Gbwz$q#E8n78v8bUpz
zBGL}BD)#;kFp{mepj0<O+JAsr^0OZ3rMflHYRQgv@QS6AX2`-Ta9UdY4JvY}NQd+u
z=>O!DaflmsXqo=QCm%OJNtO#Cm(~{jq6*2QZ{Sd#T1s)Z^&qz1q0QmxRmwox%pfE4
zjGb<lH?`$wo0Q=gtIbz2v2loz1u_<qxzCi5{N+Jr)mR*Pp3$TXVj>Q0dTLGOMi%q*
z<r%VPvzH{okQOsAWDYZ$rMi4$mC8!Dlm)4lbs>k0q*=sVNLpS`w^W<UBE+gc2hEbX
hEnqxsN3IFGw&x^Amw{2*$<w#wQ2uu5Vu}7Z^$)4dl6U|B

diff --git a/sveltejs/src/DetailModal.svelte b/sveltejs/src/DetailModal.svelte
index aa530ab20..3744187dc 100644
--- a/sveltejs/src/DetailModal.svelte
+++ b/sveltejs/src/DetailModal.svelte
@@ -58,7 +58,7 @@
   </div>
   <div class="pb-detail-modal__sidebar">
     <!-- <Image sources={project.logo} class="pb-detail-modal__project-logo" /> -->
-    {#if PACKAGE_MANAGER.available}
+    {#if PACKAGE_MANAGER}
       <div
         class="pb-detail-modal__view-commands pb-detail-modal__sidebar_element"
       >
diff --git a/sveltejs/src/Project/ActionButton.svelte b/sveltejs/src/Project/ActionButton.svelte
index e00da38a3..28f59602c 100644
--- a/sveltejs/src/Project/ActionButton.svelte
+++ b/sveltejs/src/Project/ActionButton.svelte
@@ -67,7 +67,7 @@
     </ProjectStatusIndicator>
   {:else}
     <span>
-      {#if PACKAGE_MANAGER.available && PACKAGE_MANAGER.errors.length === 0}
+      {#if PACKAGE_MANAGER}
         {#if isInInstallList && !processMultipleProjects}
           <ProjectButtonBase>
             <LoadingEllipsis />
diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte
index b525b1079..ab0c555b5 100644
--- a/sveltejs/src/ProjectBrowser.svelte
+++ b/sveltejs/src/ProjectBrowser.svelte
@@ -118,28 +118,6 @@
       if (data[source].error && data[source].error.length) {
         messenger.add(data[source].error, { type: 'error' });
       }
-
-      if (
-        PACKAGE_MANAGER.available &&
-        (PACKAGE_MANAGER.errors.length || PACKAGE_MANAGER.warnings.length)
-      ) {
-        if (PACKAGE_MANAGER.errors.length) {
-          PACKAGE_MANAGER.errors.forEach((e) => {
-            messenger.add(`Unable to download modules via the UI: ${e}`, {
-              type: 'error',
-            });
-          });
-        }
-
-        if (PACKAGE_MANAGER.warnings.length) {
-          PACKAGE_MANAGER.warnings.forEach((e) => {
-            messenger.add(
-              `There may be issues which effect downloading modules: ${e}`,
-              { type: 'warning' },
-            );
-          });
-        }
-      }
     } else {
       rows = [];
       rowsCount = 0;
diff --git a/sveltejs/src/ProjectGrid.svelte b/sveltejs/src/ProjectGrid.svelte
index 351f46e0c..cd2898b2d 100644
--- a/sveltejs/src/ProjectGrid.svelte
+++ b/sveltejs/src/ProjectGrid.svelte
@@ -72,7 +72,7 @@
       >
         <slot rows={visibleRows} />
       </ul>
-      {#if PACKAGE_MANAGER.available && processMultipleProjects}
+      {#if PACKAGE_MANAGER && processMultipleProjects}
         <ProcessInstallListButton />
       {/if}
     {/if}
diff --git a/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php b/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php
index d70305429..91b9b3038 100644
--- a/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php
+++ b/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php
@@ -6,7 +6,6 @@ namespace Drupal\project_browser_test;
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DependencyInjection\ServiceProviderBase;
-use Drupal\project_browser\InstallReadiness;
 
 /**
  * Overrides the module installer service.
@@ -17,9 +16,8 @@ class ProjectBrowserTestServiceProvider extends ServiceProviderBase {
    * {@inheritdoc}
    */
   public function alter(ContainerBuilder $container): void {
-    // The InstallReadiness service is defined by ProjectBrowserServiceProvider
-    // if Package Manager is installed.
-    if ($container->hasDefinition(InstallReadiness::class)) {
+    assert(is_array($container->getParameter('container.modules')));
+    if (array_key_exists('package_manager', $container->getParameter('container.modules'))) {
       $container->register(TestInstallReadiness::class, TestInstallReadiness::class)
         ->setAutowired(TRUE)
         ->addTag('event_subscriber');
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
index d7f459c28..b3dadd9ce 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
@@ -40,6 +40,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
     'package_manager_test_validation',
     'project_browser',
     'project_browser_test',
+    'dblog',
   ];
 
   /**
@@ -67,6 +68,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
     $this->drupalLogin($this->drupalCreateUser([
       'administer modules',
       'administer site configuration',
+      'access site reports',
     ]));
   }
 
@@ -294,14 +296,17 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
 
     $assert_session = $this->assertSession();
     $this->drupalGet('admin/modules/browse/project_browser_test_mock');
-    $settings = $this->getDrupalSettings();
-    $this->assertTrue($settings['project_browser']['package_manager']['status_checked']);
     $this->svelteInitHelper('text', 'Cream cheese on a bagel');
-    $assert_session->statusMessageContains("Simulate an error message for the project browser.", 'error');
     $cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
+    $download_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.pb__action_button");
+    $this->assertNotEmpty($download_button);
+    $this->assertSame('Install Cream cheese on a bagel', $download_button->getText());
+    $download_button->click();
+    $this->assertSame('Installing', $download_button->getText());
+    $this->assertTrue($assert_session->waitForText('Simulate an error message for the project browser.'));
     $download_button_text = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.pb__action_button")
       ?->getText();
-    $this->assertSame('View Commands for Cream cheese on a bagel', $download_button_text);
+    $this->assertSame('Install Cream cheese on a bagel', $download_button_text);
   }
 
   /**
@@ -316,7 +321,6 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
     $page = $this->getSession()->getPage();
     $this->drupalGet('admin/modules/browse/project_browser_test_mock');
     $this->svelteInitHelper('text', 'Cream cheese on a bagel');
-    $assert_session->statusMessageContains("Simulate a warning message for the project browser.", 'warning');
     $cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)';
     $download_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.pb__action_button");
     $this->assertNotEmpty($download_button);
@@ -328,6 +332,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
       return $button->getText() === 'Cream cheese on a bagel is Installed';
     });
     $this->assertTrue($installed_action);
+    $this->drupalGet('admin/reports/dblog');
+    $assert_session->pageTextContains('Simulate a warning message for the project browser.');
   }
 
   /**
-- 
GitLab