From d321d0f93c4deff1ab2d7b6e4f7faa95aa52d326 Mon Sep 17 00:00:00 2001 From: Yassine Doghri Date: Sun, 5 May 2024 13:10:59 +0000 Subject: [PATCH] refactor(plugins): redefine plugins folder structure to vendor/package --- app/Config/Autoload.php | 18 +++- app/Config/Constants.php | 3 + modules/Plugins/BasePlugin.php | 22 +++-- modules/Plugins/Commands/UninstallPlugin.php | 13 +-- modules/Plugins/Config/Routes.php | 55 +++++++----- .../Plugins/Controllers/PluginController.php | 88 ++++++++++++++----- modules/Plugins/Plugins.php | 68 ++++++++++---- themes/cp_admin/plugins/_plugin.php | 5 +- 8 files changed, 195 insertions(+), 77 deletions(-) diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php index fd633162..e4aad938 100644 --- a/app/Config/Autoload.php +++ b/app/Config/Autoload.php @@ -114,14 +114,28 @@ class Autoload extends AutoloadConfig public function __construct() { // load plugins namespaces - $pluginsPaths = glob(PLUGINS_PATH . '*', GLOB_ONLYDIR | GLOB_NOSORT); + $pluginsPaths = glob(PLUGINS_PATH . '*/*', GLOB_ONLYDIR | GLOB_NOSORT); if (! $pluginsPaths) { $pluginsPaths = []; } foreach ($pluginsPaths as $pluginPath) { - $this->psr4[sprintf('Plugins\%s', basename($pluginPath))] = $pluginPath; + $vendor = basename(dirname($pluginPath)); + $package = basename($pluginPath); + + // validate plugin pattern + if (preg_match('~' . PLUGINS_KEY_PATTERN . '~', $vendor . '/' . $package) === false) { + continue; + } + + $pluginNamespace = 'Plugins\\' . str_replace( + ' ', + '', + ucwords(str_replace(['-', '_', '.'], ' ', $vendor . '\\' . $package)) + ); + + $this->psr4[$pluginNamespace] = $pluginPath; } parent::__construct(); diff --git a/app/Config/Constants.php b/app/Config/Constants.php index fd181a82..fd5c35ff 100644 --- a/app/Config/Constants.php +++ b/app/Config/Constants.php @@ -38,6 +38,9 @@ defined('APP_NAMESPACE') || define('APP_NAMESPACE', 'App'); defined('PLUGINS_PATH') || define('PLUGINS_PATH', ROOTPATH . 'plugins' . DIRECTORY_SEPARATOR); +defined('PLUGINS_KEY_PATTERN') || + define('PLUGINS_KEY_PATTERN', '[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9]([_.-]?[a-z0-9]+)*'); + /* | -------------------------------------------------------------------------- | Composer Path diff --git a/modules/Plugins/BasePlugin.php b/modules/Plugins/BasePlugin.php index 31beca42..3e98dc98 100644 --- a/modules/Plugins/BasePlugin.php +++ b/modules/Plugins/BasePlugin.php @@ -11,6 +11,7 @@ use CodeIgniter\I18n\Time; use RuntimeException; /** + * @property string $key * @property string $name * @property string $description * @property string $version @@ -29,12 +30,13 @@ abstract class BasePlugin implements PluginInterface protected bool $active; public function __construct( - protected string $key, - protected string $filePath + protected string $vendor, + protected string $package, + protected string $directory ) { - $pluginDirectory = dirname($filePath); + $this->key = sprintf('%s/%s', $vendor, $package); - $manifest = $this->loadManifest($pluginDirectory . '/manifest.json'); + $manifest = $this->loadManifest($directory . '/manifest.json'); foreach ($manifest as $key => $value) { $this->{$key} = $value; @@ -43,7 +45,7 @@ abstract class BasePlugin implements PluginInterface // check that plugin is active $this->active = get_plugin_option($this->key, 'active') ?? false; - $this->iconSrc = $this->loadIcon($pluginDirectory . '/icon.svg'); + $this->iconSrc = $this->loadIcon($directory . '/icon.svg'); } /** @@ -88,6 +90,16 @@ abstract class BasePlugin implements PluginInterface return $this->key; } + final public function getVendor(): string + { + return $this->vendor; + } + + final public function getPackage(): string + { + return $this->package; + } + final public function getName(): string { $key = sprintf('Plugin.%s.name', $this->key); diff --git a/modules/Plugins/Commands/UninstallPlugin.php b/modules/Plugins/Commands/UninstallPlugin.php index a33f34c0..4d52772f 100644 --- a/modules/Plugins/Commands/UninstallPlugin.php +++ b/modules/Plugins/Commands/UninstallPlugin.php @@ -6,6 +6,7 @@ namespace Modules\Plugins\Commands; use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; +use Modules\Plugins\Plugins; class UninstallPlugin extends BaseCommand { @@ -51,18 +52,20 @@ class UninstallPlugin extends BaseCommand */ public function run(array $pluginKeys): int { - $validation = service('validation'); + /** @var Plugins $plugins */ + $plugins = service('plugins'); /** @var list $errors */ $errors = []; foreach ($pluginKeys as $pluginKey) { - // TODO: change validation of pluginKey - if (! $validation->check($pluginKey, 'required')) { - $errors = [...$errors, ...$validation->getErrors()]; + $plugin = $plugins->getPluginByKey($pluginKey); + + if ($plugin === null) { + $errors[] = sprintf('Plugin %s was not found.', $pluginKey); continue; } - if (! service('plugins')->uninstall($pluginKey)) { + if (! $plugins->uninstall($plugin)) { $errors[] = sprintf('Something happened when removing %s', $pluginKey); } } diff --git a/modules/Plugins/Config/Routes.php b/modules/Plugins/Config/Routes.php index 4d7ad64a..f3c1d42a 100644 --- a/modules/Plugins/Config/Routes.php +++ b/modules/Plugins/Config/Routes.php @@ -5,6 +5,9 @@ declare(strict_types=1); use CodeIgniter\Router\RouteCollection; /** @var RouteCollection $routes */ +$routes->addPlaceholder('pluginVendor', '[a-z0-9]([_.-]?[a-z0-9]+)*'); +$routes->addPlaceholder('pluginKey', PLUGINS_KEY_PATTERN); + $routes->group( config('Admin') ->gateway, @@ -17,44 +20,50 @@ $routes->group( 'as' => 'plugins-installed', 'filter' => 'permission:plugins.manage', ]); - $routes->get('(:segment)', 'PluginController::generalSettings/$1', [ - 'as' => 'plugins-general-settings', - 'filter' => 'permission:plugins.manage', - ]); - $routes->post('(:segment)', 'PluginController::generalSettingsAction/$1', [ - 'as' => 'plugins-general-settings-action', - 'filter' => 'permission:plugins.manage', - ]); - $routes->post('(:segment)/activate', 'PluginController::activate/$1', [ - 'as' => 'plugins-activate', - 'filter' => 'permission:plugins.manage', - ]); - $routes->post('(:segment)/deactivate', 'PluginController::deactivate/$1', [ - 'as' => 'plugins-deactivate', - 'filter' => 'permission:plugins.manage', - ]); - // TODO: change to delete - $routes->get('(:segment)/uninstall', 'PluginController::uninstall/$1', [ - 'as' => 'plugins-uninstall', + $routes->get('(:pluginVendor)', 'PluginController::vendor/$1', [ + 'as' => 'plugins-vendor', 'filter' => 'permission:plugins.manage', ]); + $routes->group('(:pluginKey)', static function ($routes): void { + $routes->get('/', 'PluginController::generalSettings/$1/$2', [ + 'as' => 'plugins-general-settings', + 'filter' => 'permission:plugins.manage', + ]); + $routes->post('/', 'PluginController::generalSettingsAction/$1/$2', [ + 'as' => 'plugins-general-settings-action', + 'filter' => 'permission:plugins.manage', + ]); + $routes->post('activate', 'PluginController::activate/$1/$2', [ + 'as' => 'plugins-activate', + 'filter' => 'permission:plugins.manage', + ]); + $routes->post('deactivate', 'PluginController::deactivate/$1/$2', [ + 'as' => 'plugins-deactivate', + 'filter' => 'permission:plugins.manage', + ]); + // TODO: change to delete + $routes->get('uninstall', 'PluginController::uninstall/$1/$2', [ + 'as' => 'plugins-uninstall', + 'filter' => 'permission:plugins.manage', + ]); + }); }); $routes->group('podcasts/(:num)/plugins', static function ($routes): void { - $routes->get('(:segment)', 'PluginController::podcastSettings/$1/$2', [ + $routes->get('(:pluginKey)', 'PluginController::podcastSettings/$1/$2/$3', [ 'as' => 'plugins-podcast-settings', 'filter' => 'permission:podcast#.edit', ]); - $routes->post('(:segment)', 'PluginController::podcastSettingsAction/$1/$2', [ + $routes->post('(:pluginKey)', 'PluginController::podcastSettingsAction/$1/$2/$3', [ 'as' => 'plugins-podcast-settings-action', 'filter' => 'permission:podcast#.edit', ]); }); $routes->group('podcasts/(:num)/episodes/(:num)/plugins', static function ($routes): void { - $routes->get('(:segment)', 'PluginController::episodeSettings/$1/$2/$3', [ + $routes->get('(:pluginKey)', 'PluginController::episodeSettings/$1/$2/$3/$4', [ 'as' => 'plugins-episode-settings', 'filter' => 'permission:podcast#.edit', ]); - $routes->post('(:segment)', 'PluginController::episodeSettingsAction/$1/$2/$3', [ + $routes->post('(:pluginKey)', 'PluginController::episodeSettingsAction/$1/$2/$3/$4', [ 'as' => 'plugins-episode-settings-action', 'filter' => 'permission:podcast#.edit', ]); diff --git a/modules/Plugins/Controllers/PluginController.php b/modules/Plugins/Controllers/PluginController.php index cd8b86d4..e187c376 100644 --- a/modules/Plugins/Controllers/PluginController.php +++ b/modules/Plugins/Controllers/PluginController.php @@ -35,12 +35,25 @@ class PluginController extends BaseController ]); } - public function generalSettings(string $pluginKey): string + public function vendor(string $vendor): string { /** @var Plugins $plugins */ $plugins = service('plugins'); - $plugin = $plugins->getPlugin($pluginKey); + $vendorPlugins = $plugins->getVendorPlugins($vendor); + return view('plugins/installed', [ + 'total' => count($vendorPlugins), + 'plugins' => $vendorPlugins, + 'pager_links' => '', + ]); + } + + public function generalSettings(string $vendor, string $package): string + { + /** @var Plugins $plugins */ + $plugins = service('plugins'); + + $plugin = $plugins->getPlugin($vendor, $package); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); @@ -52,12 +65,12 @@ class PluginController extends BaseController ]); } - public function generalSettingsAction(string $pluginKey): RedirectResponse + public function generalSettingsAction(string $vendor, string $package): RedirectResponse { /** @var Plugins $plugins */ $plugins = service('plugins'); - $plugin = $plugins->getPlugin($pluginKey); + $plugin = $plugins->getPlugin($vendor, $package); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); @@ -66,7 +79,7 @@ class PluginController extends BaseController foreach ($plugin->settings['general'] as $option) { $optionKey = $option['key']; $optionValue = $this->request->getPost($optionKey); - $plugins->setOption($pluginKey, $optionKey, $optionValue); + $plugins->setOption($plugin, $optionKey, $optionValue); } return redirect()->back() @@ -75,7 +88,7 @@ class PluginController extends BaseController ])); } - public function podcastSettings(string $podcastId, string $pluginKey): string + public function podcastSettings(string $podcastId, string $vendor, string $package): string { $podcast = (new PodcastModel())->getPodcastById((int) $podcastId); @@ -86,7 +99,7 @@ class PluginController extends BaseController /** @var Plugins $plugins */ $plugins = service('plugins'); - $plugin = $plugins->getPlugin($pluginKey); + $plugin = $plugins->getPlugin($vendor, $package); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); @@ -102,12 +115,12 @@ class PluginController extends BaseController ]); } - public function podcastSettingsAction(string $podcastId, string $pluginKey): RedirectResponse + public function podcastSettingsAction(string $podcastId, string $vendor, string $package): RedirectResponse { /** @var Plugins $plugins */ $plugins = service('plugins'); - $plugin = $plugins->getPlugin($pluginKey); + $plugin = $plugins->getPlugin($vendor, $package); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); @@ -116,7 +129,7 @@ class PluginController extends BaseController foreach ($plugin->settings['podcast'] as $setting) { $settingKey = $setting['key']; $settingValue = $this->request->getPost($settingKey); - $plugins->setOption($pluginKey, $settingKey, $settingValue, ['podcast', (int) $podcastId]); + $plugins->setOption($plugin, $settingKey, $settingValue, ['podcast', (int) $podcastId]); } return redirect()->back() @@ -125,7 +138,7 @@ class PluginController extends BaseController ])); } - public function episodeSettings(string $podcastId, string $episodeId, string $pluginKey): string + public function episodeSettings(string $podcastId, string $episodeId, string $vendor, string $package): string { $episode = (new EpisodeModel())->getEpisodeById((int) $episodeId); @@ -136,7 +149,7 @@ class PluginController extends BaseController /** @var Plugins $plugins */ $plugins = service('plugins'); - $plugin = $plugins->getPlugin($pluginKey); + $plugin = $plugins->getPlugin($vendor, $package); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); @@ -154,12 +167,16 @@ class PluginController extends BaseController ]); } - public function episodeSettingsAction(string $podcastId, string $episodeId, string $pluginKey): RedirectResponse - { + public function episodeSettingsAction( + string $podcastId, + string $episodeId, + string $vendor, + string $package + ): RedirectResponse { /** @var Plugins $plugins */ $plugins = service('plugins'); - $plugin = $plugins->getPlugin($pluginKey); + $plugin = $plugins->getPlugin($vendor, $package); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); @@ -168,7 +185,7 @@ class PluginController extends BaseController foreach ($plugin->settings['episode'] as $setting) { $settingKey = $setting['key']; $settingValue = $this->request->getPost($settingKey); - $plugins->setOption($pluginKey, $settingKey, $settingValue, ['episode', (int) $episodeId]); + $plugins->setOption($plugin, $settingKey, $settingValue, ['episode', (int) $episodeId]); } return redirect()->back() @@ -177,23 +194,50 @@ class PluginController extends BaseController ])); } - public function activate(string $pluginKey): RedirectResponse + public function activate(string $vendor, string $package): RedirectResponse { - service('plugins')->activate($pluginKey); + /** @var Plugins $plugins */ + $plugins = service('plugins'); + + $plugin = $plugins->getPlugin($vendor, $package); + + if ($plugin === null) { + throw PageNotFoundException::forPageNotFound(); + } + + $plugins->activate($plugin); return redirect()->back(); } - public function deactivate(string $pluginKey): RedirectResponse + public function deactivate(string $vendor, string $package): RedirectResponse { - service('plugins')->deactivate($pluginKey); + /** @var Plugins $plugins */ + $plugins = service('plugins'); + + $plugin = $plugins->getPlugin($vendor, $package); + + if ($plugin === null) { + throw PageNotFoundException::forPageNotFound(); + } + + $plugins->deactivate($plugin); return redirect()->back(); } - public function uninstall(string $pluginKey): RedirectResponse + public function uninstall(string $vendor, string $package): RedirectResponse { - service('plugins')->uninstall($pluginKey); + /** @var Plugins $plugins */ + $plugins = service('plugins'); + + $plugin = $plugins->getPlugin($vendor, $package); + + if ($plugin === null) { + throw PageNotFoundException::forPageNotFound(); + } + + $plugins->uninstall($plugin); return redirect()->back(); } diff --git a/modules/Plugins/Plugins.php b/modules/Plugins/Plugins.php index 18b6cf47..7a2af425 100644 --- a/modules/Plugins/Plugins.php +++ b/modules/Plugins/Plugins.php @@ -28,6 +28,11 @@ class Plugins */ protected static array $plugins = []; + /** + * @var array + */ + protected static array $pluginsByVendor = []; + protected static int $installedCount = 0; public function __construct() @@ -100,10 +105,18 @@ class Plugins return $pluginsWithEpisodeSettings; } - public function getPlugin(string $key): ?BasePlugin + /** + * @return array + */ + public function getVendorPlugins(string $vendor): array { - foreach (static::$plugins as $plugin) { - if ($plugin->getKey() === $key) { + return static::$pluginsByVendor[$vendor] ?? []; + } + + public function getPlugin(string $vendor, string $package): ?BasePlugin + { + foreach ($this->getVendorPlugins($vendor) as $plugin) { + if ($plugin->getKey() === $vendor . '/' . $package) { return $plugin; } } @@ -111,6 +124,16 @@ class Plugins return null; } + public function getPluginByKey(string $key): ?BasePlugin + { + if (! str_contains('/', $key)) { + return null; + } + + $keyArray = explode('/', $key); + return $this->getPlugin($keyArray[0], $keyArray[1]); + } + /** * @param value-of $name * @param array $arguments @@ -131,22 +154,22 @@ class Plugins } } - public function activate(string $pluginKey): void + public function activate(BasePlugin $plugin): void { - set_plugin_option($pluginKey, 'active', true); + set_plugin_option($plugin->getKey(), 'active', true); } - public function deactivate(string $pluginKey): void + public function deactivate(BasePlugin $plugin): void { - set_plugin_option($pluginKey, 'active', false); + set_plugin_option($plugin->getKey(), 'active', false); } /** * @param ?array{'podcast'|'episode',int} $additionalContext */ - public function setOption(string $pluginKey, string $name, mixed $value, array $additionalContext = null): void + public function setOption(BasePlugin $plugin, string $name, mixed $value, array $additionalContext = null): void { - set_plugin_option($pluginKey, $name, $value, $additionalContext); + set_plugin_option($plugin->getKey(), $name, $value, $additionalContext); } public function getInstalledCount(): int @@ -154,7 +177,7 @@ class Plugins return static::$installedCount; } - public function uninstall(string $pluginKey): bool + public function uninstall(BasePlugin $plugin): bool { // remove all settings data $db = Database::connect(); @@ -162,7 +185,7 @@ class Plugins $db->transStart(); $builder->where('class', self::class); - $builder->like('context', sprintf('plugin:%s', $pluginKey . '%')); + $builder->like('context', sprintf('plugin:%s', $plugin->getKey() . '%')); if (! $builder->delete()) { $db->transRollback(); @@ -170,7 +193,7 @@ class Plugins } // delete plugin folder from PLUGINS_PATH - $pluginFolder = PLUGINS_PATH . $pluginKey; + $pluginFolder = PLUGINS_PATH . $plugin->getKey(); $rmdirResult = $this->rrmdir($pluginFolder); $transResult = $db->transCommit(); @@ -181,27 +204,36 @@ class Plugins protected function registerPlugins(): void { // search for plugins in plugins folder - // TODO: only get directories? Should be organized as author/repo? - $pluginsFiles = glob(PLUGINS_PATH . '**/Plugin.php'); + $pluginsDirectories = glob(PLUGINS_PATH . '*/*', GLOB_ONLYDIR); - if (! $pluginsFiles) { + if ($pluginsDirectories === false || $pluginsDirectories === []) { return; } $locator = service('locator'); - foreach ($pluginsFiles as $file) { - $className = $locator->findQualifiedNameFromPath($file); + foreach ($pluginsDirectories as $pluginDirectory) { + $vendor = basename(dirname($pluginDirectory)); + $package = basename($pluginDirectory); + + if (preg_match('~' . PLUGINS_KEY_PATTERN . '~', $vendor . '/' . $package) === false) { + continue; + } + + $pluginFile = $pluginDirectory . DIRECTORY_SEPARATOR . 'Plugin.php'; + + $className = $locator->findQualifiedNameFromPath($pluginFile); if ($className === false) { continue; } - $plugin = new $className(basename(dirname($file)), $file); + $plugin = new $className($vendor, $package, $pluginDirectory); if (! $plugin instanceof BasePlugin) { continue; } static::$plugins[] = $plugin; + static::$pluginsByVendor[$vendor][] = $plugin; ++static::$installedCount; } } diff --git a/themes/cp_admin/plugins/_plugin.php b/themes/cp_admin/plugins/_plugin.php index 5f4b5f01..9cb7e088 100644 --- a/themes/cp_admin/plugins/_plugin.php +++ b/themes/cp_admin/plugins/_plugin.php @@ -6,9 +6,10 @@ ]) ?> -
+

getName() ?>version ?>

-

getDescription() ?>

+

getVendor() ?>/getPackage() ?>

+

getDescription() ?>