From a80e364841e2920ffde18a2f73e314f3a9e4992d Mon Sep 17 00:00:00 2001
From: Marcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>
Date: Fri, 2 May 2025 13:18:34 +0200
Subject: [PATCH] Allow plugins to provide cli scripts.

---
 cli/commands.php |  60 +++++++++++++++++++++
 cli/studip       | 134 +++++++++++++++++++++++++----------------------
 2 files changed, 132 insertions(+), 62 deletions(-)
 create mode 100644 cli/commands.php

diff --git a/cli/commands.php b/cli/commands.php
new file mode 100644
index 00000000000..55ff5f78a5a
--- /dev/null
+++ b/cli/commands.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Studip\Cli;
+
+return [
+    Commands\Base\Dump::class,
+    Commands\Base\Tinker::class,
+    Commands\Checks\Compatibility::class,
+    Commands\Checks\HelpTours::class,
+    Commands\Checks\HelpTours::class,
+    Commands\CleanupAdmissionRules::class,
+    Commands\Composer\GenerateUpdateList::class,
+    Commands\Config\ConfigList::class,
+    Commands\Config\GetConfigValue::class,
+    Commands\Config\SectionList::class,
+    Commands\Config\SetConfigValue::class,
+    Commands\Course\GetCourse::class,
+    Commands\Cronjobs\CronjobExecute::class,
+    Commands\Cronjobs\CronjobExecute::class,
+    Commands\Cronjobs\CronjobList::class,
+    Commands\Cronjobs\CronjobList::class,
+    Commands\Cronjobs\CronjobWorker::class,
+    Commands\Cronjobs\CronjobWorker::class,
+    Commands\DB\Dump::class,
+    Commands\DB\MoveMatrikelnummer::class,
+    Commands\Make\Migration::class,
+    Commands\Make\Model::class,
+    Commands\Make\Plugin::class,
+    Commands\DI\Reset::class,
+    Commands\Files\Dump::class,
+    Commands\Fix\Biest7789::class,
+    Commands\Fix\Biest7866::class,
+    Commands\Fix\Biest8136::class,
+    Commands\Fix\IconDimensions::class,
+    Commands\HelpContent\Migrate::class,
+    Commands\Migrate\Migrate::class,
+    Commands\Migrate\MigrateList::class,
+    Commands\Migrate\MigrateStatus::class,
+    Commands\OAuth2\Keys::class,
+    Commands\OAuth2\Purge::class,
+    Commands\Plugins\I18N\I18NCompile::class,
+    Commands\Plugins\I18N\I18NDetect::class,
+    Commands\Plugins\I18N\I18NExtract::class,
+    Commands\Plugins\PluginActivate::class,
+    Commands\Plugins\PluginDeactivate::class,
+    Commands\Plugins\PluginInfo::class,
+    Commands\Plugins\PluginInstall::class,
+    Commands\Plugins\PluginListMigrations::class,
+    Commands\Plugins\PluginMigrate::class,
+    Commands\Plugins\PluginRegister::class,
+    Commands\Plugins\PluginScan::class,
+    Commands\Plugins\PluginStatusMigrations::class,
+    Commands\Plugins\PluginUnregister::class,
+    Commands\Resources\UpdateBookingIntervals::class,
+    Commands\SORM\DescribeModels::class,
+    Commands\Twillo\PrivateKeys::class,
+    Commands\User\ChangePassword::class,
+    Commands\User\GetUser::class,
+    Commands\User\UsersDelete::class,
+];
diff --git a/cli/studip b/cli/studip
index 8effcae08c7..c64aaf2feb4 100755
--- a/cli/studip
+++ b/cli/studip
@@ -5,70 +5,80 @@ namespace Studip\Cli;
 
 use Symfony\Component\Console\Application;
 
-require __DIR__.'/studip_cli_env.inc.php';
-require __DIR__.'/../composer/autoload.php';
+require __DIR__ . '/studip_cli_env.inc.php';
+require __DIR__ . '/../composer/autoload.php';
 
 \StudipAutoloader::addAutoloadPath('cli', 'Studip\\Cli');
 
 $application = new Application();
-$commands = [
-    Commands\Base\Dump::class,
-    Commands\Base\Tinker::class,
-    Commands\Checks\Compatibility::class,
-    Commands\Checks\HelpTours::class,
-    Commands\Checks\HelpTours::class,
-    Commands\CleanupAdmissionRules::class,
-    Commands\Composer\GenerateUpdateList::class,
-    Commands\Config\ConfigList::class,
-    Commands\Config\GetConfigValue::class,
-    Commands\Config\SectionList::class,
-    Commands\Config\SetConfigValue::class,
-    Commands\Course\GetCourse::class,
-    Commands\Cronjobs\CronjobExecute::class,
-    Commands\Cronjobs\CronjobExecute::class,
-    Commands\Cronjobs\CronjobList::class,
-    Commands\Cronjobs\CronjobList::class,
-    Commands\Cronjobs\CronjobWorker::class,
-    Commands\Cronjobs\CronjobWorker::class,
-    Commands\DB\Dump::class,
-    Commands\DB\MoveMatrikelnummer::class,
-    Commands\Make\Migration::class,
-    Commands\Make\Model::class,
-    Commands\Make\Plugin::class,
-    Commands\DI\Reset::class,
-    Commands\Files\Dump::class,
-    Commands\Fix\Biest7789::class,
-    Commands\Fix\Biest7866::class,
-    Commands\Fix\Biest8136::class,
-    Commands\Fix\IconDimensions::class,
-    Commands\HelpContent\Migrate::class,
-    Commands\Migrate\Migrate::class,
-    Commands\Migrate\MigrateList::class,
-    Commands\Migrate\MigrateStatus::class,
-    Commands\OAuth2\Keys::class,
-    Commands\OAuth2\Purge::class,
-    Commands\Plugins\I18N\I18NCompile::class,
-    Commands\Plugins\I18N\I18NDetect::class,
-    Commands\Plugins\I18N\I18NExtract::class,
-    Commands\Plugins\PluginActivate::class,
-    Commands\Plugins\PluginDeactivate::class,
-    Commands\Plugins\PluginInfo::class,
-    Commands\Plugins\PluginInstall::class,
-    Commands\Plugins\PluginListMigrations::class,
-    Commands\Plugins\PluginMigrate::class,
-    Commands\Plugins\PluginRegister::class,
-    Commands\Plugins\PluginScan::class,
-    Commands\Plugins\PluginStatusMigrations::class,
-    Commands\Plugins\PluginUnregister::class,
-    Commands\Resources\UpdateBookingIntervals::class,
-    Commands\SORM\DescribeModels::class,
-    Commands\Twillo\PrivateKeys::class,
-    Commands\User\ChangePassword::class,
-    Commands\User\GetUser::class,
-    Commands\User\UsersDelete::class,
-];
-$creator = function ($command) {
-    return app($command);
-};
-$application->addCommands(array_map($creator, $commands));
+$application->addCommands(loadCoreCommands());
+$application->addCommands(loadPluginCommands());
 $application->run();
+
+function loadCoreCommands(): array
+{
+    $commands = require __DIR__ . '/commands.php';
+    return array_map(fn($command) => app($command), $commands);
+}
+
+function loadPluginCommands(): array
+{
+    $pluginCommands = [];
+    foreach (scanPluginDirectory() as $manifest) {
+        $pluginCommands = array_merge($pluginCommands, getPluginCommands($manifest));
+    }
+    return $pluginCommands;
+}
+
+function scanPluginDirectory(): \Generator
+{
+    $basepath = \Config::get()->PLUGINS_PATH;
+    $pluginManager = \PluginManager::getInstance();
+    $iterator = createPluginManifestIterator($basepath);
+
+    foreach ($iterator as $manifestFile) {
+        $manifest = $pluginManager->getPluginManifest($manifestFile->getPath());
+        if (isValidPluginManifest($manifest, $basepath, $manifestFile)) {
+            $manifest['path'] = $manifestFile->getPath();
+            yield $manifest;
+        }
+    }
+}
+
+function createPluginManifestIterator(string $basepath): \RegexIterator
+{
+    return new \RegexIterator(
+        new \RecursiveIteratorIterator(
+            new \RecursiveDirectoryIterator(
+                $basepath,
+                \FilesystemIterator::FOLLOW_SYMLINKS | \FilesystemIterator::UNIX_PATHS
+            )
+        ),
+        '/\/plugin\.manifest$/',
+        \RegexIterator::MATCH
+    );
+}
+
+function isValidPluginManifest(array $manifest, string $basepath, \SplFileInfo $manifestFile): bool
+{
+    if (!isset($manifest['pluginclassname'], $manifest['cli'])) {
+        return false;
+    }
+
+    $pluginpath = $basepath . '/' . $manifest['origin'] . '/' . $manifest['pluginclassname'];
+    return $pluginpath === $manifestFile->getPath();
+}
+
+function getPluginCommands(array $manifest): array
+{
+    $cliFile = $manifest['path'] . '/' . $manifest['cli'];
+    $commandsFn = require_once $cliFile;
+    $commands = [];
+
+    foreach ($commandsFn() as $class => $path) {
+        require_once $path;
+        $commands[] = app($class);
+    }
+
+    return $commands;
+}
-- 
GitLab