From 544ffd5a63d4221f77c9ed32994857717eb1bb25 Mon Sep 17 00:00:00 2001
From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org>
Date: Thu, 20 Jun 2024 16:47:50 +0000
Subject: [PATCH] Issue #3455477: The recipe source should be able to show a
 particular list of curated projects

---
 .../project_browser.admin_settings.yml        |  1 +
 config/schema/project_browser.schema.yml      | 11 ++++++++
 src/Plugin/ProjectBrowserSource/Recipes.php   | 14 +++++++++-
 tests/src/Kernel/RecipesSourceTest.php        | 27 +++++++++++++++++++
 4 files changed, 52 insertions(+), 1 deletion(-)

diff --git a/config/install/project_browser.admin_settings.yml b/config/install/project_browser.admin_settings.yml
index 522694b65..f27dc0e89 100644
--- a/config/install/project_browser.admin_settings.yml
+++ b/config/install/project_browser.admin_settings.yml
@@ -2,3 +2,4 @@ enabled_sources:
   - drupalorg_mockapi
 allow_ui_install: false
 disable_add_new_module: true
+allowed_projects: {}
diff --git a/config/schema/project_browser.schema.yml b/config/schema/project_browser.schema.yml
index 91611f246..941144443 100644
--- a/config/schema/project_browser.schema.yml
+++ b/config/schema/project_browser.schema.yml
@@ -14,3 +14,14 @@ project_browser.admin_settings:
     disable_add_new_module:
       type: boolean
       label: 'Disable Add new module menu item'
+    allowed_projects:
+      type: sequence
+      label: 'Allow-lists of projects, keyed by source plugin ID'
+      sequence:
+        type: sequence
+        label: 'List of allowed projects'
+        sequence:
+          type: string
+          label: 'Project identifier'
+          constraints:
+            NotBlank: []
diff --git a/src/Plugin/ProjectBrowserSource/Recipes.php b/src/Plugin/ProjectBrowserSource/Recipes.php
index 54e0bad5c..cfcab182c 100644
--- a/src/Plugin/ProjectBrowserSource/Recipes.php
+++ b/src/Plugin/ProjectBrowserSource/Recipes.php
@@ -8,6 +8,7 @@ use Composer\InstalledVersions;
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Serialization\Yaml;
 use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Extension\ModuleExtensionList;
 use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\File\FileUrlGeneratorInterface;
@@ -30,6 +31,7 @@ class Recipes extends ProjectBrowserSourceBase {
     private readonly CacheBackendInterface $cacheBin,
     private readonly ModuleExtensionList $moduleList,
     private readonly FileUrlGeneratorInterface $fileUrlGenerator,
+    private readonly ConfigFactoryInterface $configFactory,
     private readonly string $appRoot,
     mixed ...$arguments,
   ) {
@@ -45,6 +47,7 @@ class Recipes extends ProjectBrowserSourceBase {
       $container->get('cache.project_browser'),
       $container->get(ModuleExtensionList::class),
       $container->get(FileUrlGeneratorInterface::class),
+      $container->get(ConfigFactoryInterface::class),
       $container->getParameter('app.root'),
       ...array_slice(func_get_args(), 1),
     );
@@ -175,13 +178,22 @@ class Recipes extends ProjectBrowserSourceBase {
       $search_in[] = dirname($path);
     }
 
-    return Finder::create()
+    $finder = Finder::create()
       ->files()
       ->in($search_in)
       ->depth(1)
       // The example recipe exists for documentation purposes only.
       ->notPath('example/')
       ->name('recipe.yml');
+
+    $allowed = $this->configFactory->get('project_browser.admin_settings')
+      ->get('allowed_projects.' . $this->getPluginId());
+    if ($allowed) {
+      $finder->path(
+        array_map(fn (string $name) => $name . '/', $allowed),
+      );
+    }
+    return $finder;
   }
 
   /**
diff --git a/tests/src/Kernel/RecipesSourceTest.php b/tests/src/Kernel/RecipesSourceTest.php
index bc1c723e0..c31b30e67 100644
--- a/tests/src/Kernel/RecipesSourceTest.php
+++ b/tests/src/Kernel/RecipesSourceTest.php
@@ -105,4 +105,31 @@ class RecipesSourceTest extends KernelTestBase {
     $this->assertNotEmpty($body);
   }
 
+  /**
+   * Tests that discovered recipes are limited by an allow-list.
+   */
+  public function testAllowList(): void {
+    $expected_recipe_names = ['document_media_type', 'user_picture'];
+
+    $this->config('project_browser.admin_settings')
+      ->set('allowed_projects', [
+        'recipes' => ['example', ...$expected_recipe_names],
+      ])
+      ->save();
+
+    /** @var \Drupal\project_browser\ProjectBrowser\ProjectsResultsPage $projects */
+    $projects = $this->container->get(ProjectBrowserSourceManager::class)
+      ->createInstance('recipes')
+      ->getProjects();
+    $found_recipe_names = array_column($projects->list, 'machineName');
+
+    // The `example` recipe (from core) should always be hidden, even if it's in
+    // the allow list.
+    $this->assertNotContains('example', $found_recipe_names);
+
+    sort($expected_recipe_names);
+    sort($found_recipe_names);
+    $this->assertSame($expected_recipe_names, $found_recipe_names);
+  }
+
 }
-- 
GitLab