mirror of
https://code.castopod.org/adaures/castopod
synced 2025-06-05 08:52:00 +00:00
feat(plugins): uninstall plugins via CLI and admin UI
This commit is contained in:
parent
89ac92fb41
commit
9a80de4068
@ -114,7 +114,7 @@ class Autoload extends AutoloadConfig
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
// load plugins namespaces
|
// load plugins namespaces
|
||||||
$pluginsPaths = glob(ROOTPATH . '/plugins/*', GLOB_ONLYDIR | GLOB_NOSORT);
|
$pluginsPaths = glob(PLUGINS_PATH . '*', GLOB_ONLYDIR | GLOB_NOSORT);
|
||||||
|
|
||||||
if (! $pluginsPaths) {
|
if (! $pluginsPaths) {
|
||||||
$pluginsPaths = [];
|
$pluginsPaths = [];
|
||||||
|
@ -28,6 +28,16 @@ defined('CP_VERSION') || define('CP_VERSION', '1.11.0');
|
|||||||
*/
|
*/
|
||||||
defined('APP_NAMESPACE') || define('APP_NAMESPACE', 'App');
|
defined('APP_NAMESPACE') || define('APP_NAMESPACE', 'App');
|
||||||
|
|
||||||
|
/*
|
||||||
|
| --------------------------------------------------------------------
|
||||||
|
| Plugins Path
|
||||||
|
| --------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This defines the folder in which plugins will live.
|
||||||
|
*/
|
||||||
|
defined('PLUGINS_PATH') ||
|
||||||
|
define('PLUGINS_PATH', ROOTPATH . 'plugins' . DIRECTORY_SEPARATOR);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
| --------------------------------------------------------------------------
|
| --------------------------------------------------------------------------
|
||||||
| Composer Path
|
| Composer Path
|
||||||
|
@ -5,15 +5,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ring-accent {
|
|
||||||
@apply outline-none ring-2 ring-offset-2;
|
|
||||||
|
|
||||||
/* FIXME: why doesn't ring-accent-base work? */
|
|
||||||
--tw-ring-opacity: 1;
|
|
||||||
--tw-ring-color: hsl(var(--color-accent-base) / var(--tw-ring-opacity));
|
|
||||||
--tw-ring-offset-color: hsl(var(--color-background-base));
|
|
||||||
}
|
|
||||||
|
|
||||||
.rounded-conditional-b-xl {
|
.rounded-conditional-b-xl {
|
||||||
border-bottom-right-radius: max(
|
border-bottom-right-radius: max(
|
||||||
0px,
|
0px,
|
||||||
|
@ -28,12 +28,12 @@ class Button extends Component
|
|||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
$baseClass =
|
$baseClass =
|
||||||
'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full focus:ring-accent';
|
'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full';
|
||||||
|
|
||||||
$variantClass = [
|
$variantClass = [
|
||||||
'default' => 'shadow-sm text-black bg-gray-300 hover:bg-gray-400',
|
'default' => 'shadow-sm text-black bg-gray-300 hover:bg-gray-400',
|
||||||
'primary' => 'shadow-sm text-accent-contrast bg-accent-base hover:bg-accent-hover',
|
'primary' => 'shadow-sm text-accent-contrast bg-accent-base hover:bg-accent-hover',
|
||||||
'secondary' => 'shadow-sm border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
|
'secondary' => 'shadow-sm ring-2 ring-accent ring-inset text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
|
||||||
'success' => 'shadow-sm text-white bg-pine-500 hover:bg-pine-800',
|
'success' => 'shadow-sm text-white bg-pine-500 hover:bg-pine-800',
|
||||||
'danger' => 'shadow-sm text-white bg-red-600 hover:bg-red-700',
|
'danger' => 'shadow-sm text-white bg-red-600 hover:bg-red-700',
|
||||||
'warning' => 'shadow-sm text-black bg-yellow-500 hover:bg-yellow-600',
|
'warning' => 'shadow-sm text-black bg-yellow-500 hover:bg-yellow-600',
|
||||||
|
@ -37,7 +37,7 @@ class DropdownMenu extends Component
|
|||||||
switch ($item['type']) {
|
switch ($item['type']) {
|
||||||
case 'link':
|
case 'link':
|
||||||
$menuItems .= anchor($item['uri'], $item['title'], [
|
$menuItems .= anchor($item['uri'], $item['title'], [
|
||||||
'class' => 'px-4 py-1 hover:bg-highlight focus:ring-accent focus:ring-inset' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''),
|
'class' => 'inline-flex gap-x-1 items-center px-4 py-1 hover:bg-highlight focus:ring-accent focus:ring-inset' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''),
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case 'html':
|
case 'html':
|
||||||
|
76
modules/Plugins/Commands/UninstallPlugin.php
Normal file
76
modules/Plugins/Commands/UninstallPlugin.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\Plugins\Commands;
|
||||||
|
|
||||||
|
use CodeIgniter\CLI\BaseCommand;
|
||||||
|
use CodeIgniter\CLI\CLI;
|
||||||
|
|
||||||
|
class UninstallPlugin extends BaseCommand
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The Command's Group
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $group = 'Plugins';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Command's Name
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $name = 'plugins:uninstall';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Command's Description
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Command's Usage
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $usage = 'plugins:uninstall [plugins]';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Command's Arguments
|
||||||
|
*
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
protected $arguments = [
|
||||||
|
'plugins' => 'One or more plugins as vendor/plugin',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $pluginKeys
|
||||||
|
*/
|
||||||
|
public function run(array $pluginKeys): int
|
||||||
|
{
|
||||||
|
$validation = service('validation');
|
||||||
|
|
||||||
|
/** @var list<string> $errors */
|
||||||
|
$errors = [];
|
||||||
|
foreach ($pluginKeys as $pluginKey) {
|
||||||
|
// TODO: change validation of pluginKey
|
||||||
|
if (! $validation->check($pluginKey, 'required')) {
|
||||||
|
$errors = [...$errors, ...$validation->getErrors()];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! service('plugins')->uninstall($pluginKey)) {
|
||||||
|
$errors[] = sprintf('Something happened when removing %s', $pluginKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($errors as $error) {
|
||||||
|
CLI::error($error . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors === [] ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
@ -25,14 +25,19 @@ $routes->group(
|
|||||||
'as' => 'plugins-general-settings-action',
|
'as' => 'plugins-general-settings-action',
|
||||||
'filter' => 'permission:plugins.manage',
|
'filter' => 'permission:plugins.manage',
|
||||||
]);
|
]);
|
||||||
$routes->post('activate/(:segment)', 'PluginController::activate/$1', [
|
$routes->post('(:segment)/activate', 'PluginController::activate/$1', [
|
||||||
'as' => 'plugins-activate',
|
'as' => 'plugins-activate',
|
||||||
'filter' => 'permission:plugins.manage',
|
'filter' => 'permission:plugins.manage',
|
||||||
]);
|
]);
|
||||||
$routes->post('deactivate/(:segment)', 'PluginController::deactivate/$1', [
|
$routes->post('(:segment)/deactivate', 'PluginController::deactivate/$1', [
|
||||||
'as' => 'plugins-deactivate',
|
'as' => 'plugins-deactivate',
|
||||||
'filter' => 'permission:plugins.manage',
|
'filter' => 'permission:plugins.manage',
|
||||||
]);
|
]);
|
||||||
|
// TODO: change to delete
|
||||||
|
$routes->get('(:segment)/uninstall', 'PluginController::uninstall/$1', [
|
||||||
|
'as' => 'plugins-uninstall',
|
||||||
|
'filter' => 'permission:plugins.manage',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
$routes->group('podcasts/(:num)/plugins', static function ($routes): void {
|
$routes->group('podcasts/(:num)/plugins', static function ($routes): void {
|
||||||
$routes->get('(:segment)', 'PluginController::podcastSettings/$1/$2', [
|
$routes->get('(:segment)', 'PluginController::podcastSettings/$1/$2', [
|
||||||
|
@ -190,4 +190,11 @@ class PluginController extends BaseController
|
|||||||
|
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function uninstall(string $pluginKey): RedirectResponse
|
||||||
|
{
|
||||||
|
service('plugins')->uninstall($pluginKey);
|
||||||
|
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ return [
|
|||||||
'settings' => '{pluginName} settings',
|
'settings' => '{pluginName} settings',
|
||||||
'activate' => 'Activate',
|
'activate' => 'Activate',
|
||||||
'deactivate' => 'Deactivate',
|
'deactivate' => 'Deactivate',
|
||||||
|
'uninstall' => 'Uninstall',
|
||||||
'keywords' => [
|
'keywords' => [
|
||||||
'podcasting20' => 'Podcasting 2.0',
|
'podcasting20' => 'Podcasting 2.0',
|
||||||
'seo' => 'SEO',
|
'seo' => 'SEO',
|
||||||
|
@ -7,6 +7,7 @@ namespace Modules\Plugins;
|
|||||||
use App\Entities\Episode;
|
use App\Entities\Episode;
|
||||||
use App\Entities\Podcast;
|
use App\Entities\Podcast;
|
||||||
use App\Libraries\SimpleRSSElement;
|
use App\Libraries\SimpleRSSElement;
|
||||||
|
use Config\Database;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method void channelTag(Podcast $podcast, SimpleRSSElement $channel)
|
* @method void channelTag(Podcast $podcast, SimpleRSSElement $channel)
|
||||||
@ -153,11 +154,35 @@ class Plugins
|
|||||||
return static::$installedCount;
|
return static::$installedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function uninstall(string $pluginKey): bool
|
||||||
|
{
|
||||||
|
// remove all settings data
|
||||||
|
$db = Database::connect();
|
||||||
|
$builder = $db->table('settings');
|
||||||
|
|
||||||
|
$db->transStart();
|
||||||
|
$builder->where('class', self::class);
|
||||||
|
$builder->like('context', sprintf('plugin:%s', $pluginKey . '%'));
|
||||||
|
|
||||||
|
if (! $builder->delete()) {
|
||||||
|
$db->transRollback();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete plugin folder from PLUGINS_PATH
|
||||||
|
$pluginFolder = PLUGINS_PATH . $pluginKey;
|
||||||
|
$rmdirResult = $this->rrmdir($pluginFolder);
|
||||||
|
|
||||||
|
$transResult = $db->transCommit();
|
||||||
|
|
||||||
|
return $rmdirResult && $transResult;
|
||||||
|
}
|
||||||
|
|
||||||
protected function registerPlugins(): void
|
protected function registerPlugins(): void
|
||||||
{
|
{
|
||||||
// search for plugins in plugins folder
|
// search for plugins in plugins folder
|
||||||
// TODO: only get directories? Should be organized as author/repo?
|
// TODO: only get directories? Should be organized as author/repo?
|
||||||
$pluginsFiles = glob(ROOTPATH . '/plugins/**/Plugin.php');
|
$pluginsFiles = glob(PLUGINS_PATH . '**/Plugin.php');
|
||||||
|
|
||||||
if (! $pluginsFiles) {
|
if (! $pluginsFiles) {
|
||||||
return;
|
return;
|
||||||
@ -180,4 +205,38 @@ class Plugins
|
|||||||
++static::$installedCount;
|
++static::$installedCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapted from https://stackoverflow.com/a/3338133
|
||||||
|
*/
|
||||||
|
private function rrmdir(string $dir): bool
|
||||||
|
{
|
||||||
|
if (! is_dir($dir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$objects = scandir($dir);
|
||||||
|
|
||||||
|
if (! $objects) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($objects as $object) {
|
||||||
|
if ($object === '.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($object === '..') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_dir($dir . DIRECTORY_SEPARATOR . $object) && ! is_link($dir . '/' . $object)) {
|
||||||
|
$this->rrmdir($dir . DIRECTORY_SEPARATOR . $object);
|
||||||
|
} else {
|
||||||
|
unlink($dir . DIRECTORY_SEPARATOR . $object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rmdir($dir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,28 @@
|
|||||||
<a href="<?= $plugin->website ?>" class="inline-flex items-center text-sm font-semibold underline hover:no-underline gap-x-1" target="_blank" rel="noopener noreferrer"><?= icon('link', [
|
<a href="<?= $plugin->website ?>" class="inline-flex items-center text-sm font-semibold underline hover:no-underline gap-x-1" target="_blank" rel="noopener noreferrer"><?= icon('link', [
|
||||||
'class' => 'text-gray-500',
|
'class' => 'text-gray-500',
|
||||||
]) . lang('Plugins.website') ?></a>
|
]) . lang('Plugins.website') ?></a>
|
||||||
|
<div class="flex gap-x-2">
|
||||||
<?php if($plugin->isActive()): ?>
|
<?php if($plugin->isActive()): ?>
|
||||||
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-deactivate', $plugin->getKey()) ?>">
|
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-deactivate', $plugin->getKey()) ?>">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<Button type="submit" variant="danger" size="small"><?= lang('Plugins.deactivate') ?></Button>
|
<Button type="submit" variant="danger" size="small"><?= lang('Plugins.deactivate') ?></Button>
|
||||||
</form>
|
</form>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-activate', $plugin->getKey()) ?>">
|
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-activate', $plugin->getKey()) ?>">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<Button type="submit" variant="secondary" size="small"><?= lang('Plugins.activate') ?></Button>
|
<Button type="submit" variant="secondary" size="small"><?= lang('Plugins.activate') ?></Button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<button class="p-2 rounded-full" id="more-dropdown-<?= $plugin->getKey() ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $plugin->getKey() ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button>
|
||||||
|
<?php $items = [[
|
||||||
|
'type' => 'link',
|
||||||
|
'title' => icon('delete-bin-fill', [
|
||||||
|
'class' => 'text-gray-500',
|
||||||
|
]) . lang('Plugins.uninstall'),
|
||||||
|
'uri' => route_to('plugins-uninstall', $plugin->getKey()),
|
||||||
|
'class' => 'font-semibold text-red-600',
|
||||||
|
]]; ?>
|
||||||
|
<DropdownMenu id="more-dropdown-<?= $plugin->getKey() ?>-menu" labelledby="more-dropdown-<?= $plugin->getKey() ?>" placement="top-end" offsetY="-32" items="<?= esc(json_encode($items)) ?>" />
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
Loading…
x
Reference in New Issue
Block a user