From 8aa23ccc9759795891653d7f6d2fe035b9ac952e Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 7 Nov 2025 09:21:30 +0100 Subject: [PATCH] add uninstall & remove --- .../Plugin/UninstallPluginCommand.php | 39 +++++ app/Facades/Plugins.php | 3 + .../Resources/Plugins/PluginResource.php | 156 +++++++++++------- app/Services/Helpers/PluginService.php | 47 ++++++ lang/en/admin/plugin.php | 3 + 5 files changed, 189 insertions(+), 59 deletions(-) create mode 100644 app/Console/Commands/Plugin/UninstallPluginCommand.php diff --git a/app/Console/Commands/Plugin/UninstallPluginCommand.php b/app/Console/Commands/Plugin/UninstallPluginCommand.php new file mode 100644 index 000000000..5bb720586 --- /dev/null +++ b/app/Console/Commands/Plugin/UninstallPluginCommand.php @@ -0,0 +1,39 @@ +argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray()); + + $plugin = Plugin::find($id); + + if (!$plugin) { + $this->error('Plugin does not exist!'); + + return; + } + + if (!$plugin->isInstalled()) { + $this->error('Plugin is not installed!'); + + return; + } + + $deleteFiles = $this->option('delete') ?? $this->confirm('Do you also want to delete the plugin files?'); + + Plugins::uninstallPlugin($plugin, $deleteFiles); + + $this->info('Plugin uninstalled' . ($deleteFiles ? ' and files deleted' : '') . '.'); + } +} diff --git a/app/Facades/Plugins.php b/app/Facades/Plugins.php index 66f2ada30..fa84505d2 100644 --- a/app/Facades/Plugins.php +++ b/app/Facades/Plugins.php @@ -12,8 +12,11 @@ use Illuminate\Support\Facades\Facade; * @method static void loadPlugins() * @method static void loadPanelPlugins(Panel $panel) * @method static void requireComposerPackages(Plugin $plugin) + * @method static void removeComposerPackages(Plugin $plugin) * @method static void runPluginMigrations(Plugin $plugin) + * @method static void rollbackPluginMigrations(Plugin $plugin) * @method static void installPlugin(Plugin $plugin, bool $enable = true) + * @method static void uninstallPlugin(Plugin $plugin, bool $deleteFiles = false) * @method static void updatePlugin(Plugin $plugin) * @method static void downloadPluginFromFile(UploadedFile $file) * @method static void downloadPluginFromUrl(string $url) diff --git a/app/Filament/Admin/Resources/Plugins/PluginResource.php b/app/Filament/Admin/Resources/Plugins/PluginResource.php index 2d8595faf..64002d76b 100644 --- a/app/Filament/Admin/Resources/Plugins/PluginResource.php +++ b/app/Filament/Admin/Resources/Plugins/PluginResource.php @@ -7,6 +7,7 @@ use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins; use App\Models\Plugin; use Exception; use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\TextInput; use Filament\Infolists\Components\TextEntry; @@ -17,6 +18,7 @@ use Filament\Schemas\Components\Tabs\Tab; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\File; class PluginResource extends Resource { @@ -109,73 +111,109 @@ class PluginResource extends Resource ->schema(fn (Plugin $plugin) => $plugin->getSettingsForm()) ->action(fn (array $data, Plugin $plugin) => $plugin->saveSettings($data)) ->slideOver(), - Action::make('install') - ->label(trans('admin/plugin.install')) - ->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin)) - ->icon('tabler-terminal') - ->color('success') - ->hidden(fn (Plugin $plugin) => $plugin->isInstalled()) - ->action(function (Plugin $plugin, $livewire) { - Plugins::installPlugin($plugin, !$plugin->isTheme() || !Plugins::hasThemePluginEnabled()); + ActionGroup::make([ + Action::make('install') + ->label(trans('admin/plugin.install')) + ->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin)) + ->icon('tabler-terminal') + ->color('success') + ->hidden(fn (Plugin $plugin) => $plugin->isInstalled()) + ->action(function (Plugin $plugin, $livewire) { + Plugins::installPlugin($plugin, !$plugin->isTheme() || !Plugins::hasThemePluginEnabled()); - redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab])); + redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab])); - Notification::make() - ->success() - ->title(trans('admin/plugin.notifications.installed')) - ->send(); - }), - Action::make('update') - ->label(trans('admin/plugin.update')) - ->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin)) - ->icon('tabler-download') - ->color('success') - ->visible(fn (Plugin $plugin) => $plugin->isUpdateAvailable()) - ->action(function (Plugin $plugin, $livewire) { - Plugins::updatePlugin($plugin); + Notification::make() + ->success() + ->title(trans('admin/plugin.notifications.installed')) + ->send(); + }), + Action::make('update') + ->label(trans('admin/plugin.update')) + ->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin)) + ->icon('tabler-download') + ->color('success') + ->visible(fn (Plugin $plugin) => $plugin->isInstalled() && $plugin->isUpdateAvailable()) + ->action(function (Plugin $plugin, $livewire) { + Plugins::updatePlugin($plugin); - redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab])); + redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab])); - Notification::make() - ->success() - ->title(trans('admin/plugin.notifications.updated')) - ->send(); - }), - Action::make('enable') - ->label(trans('admin/plugin.enable')) - ->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin)) - ->icon('tabler-check') - ->color('success') - ->visible(fn (Plugin $plugin) => $plugin->canEnable()) - ->requiresConfirmation(fn (Plugin $plugin) => $plugin->isTheme() && Plugins::hasThemePluginEnabled()) - ->modalHeading(fn (Plugin $plugin) => $plugin->isTheme() && Plugins::hasThemePluginEnabled() ? trans('admin/plugin.enable_theme_modal.heading') : null) - ->modalDescription(fn (Plugin $plugin) => $plugin->isTheme() && Plugins::hasThemePluginEnabled() ? trans('admin/plugin.enable_theme_modal.description') : null) - ->action(function (Plugin $plugin, $livewire) { - Plugins::enablePlugin($plugin); + Notification::make() + ->success() + ->title(trans('admin/plugin.notifications.updated')) + ->send(); + }), + Action::make('enable') + ->label(trans('admin/plugin.enable')) + ->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin)) + ->icon('tabler-check') + ->color('success') + ->visible(fn (Plugin $plugin) => $plugin->canEnable()) + ->requiresConfirmation(fn (Plugin $plugin) => $plugin->isTheme() && Plugins::hasThemePluginEnabled()) + ->modalHeading(fn (Plugin $plugin) => $plugin->isTheme() && Plugins::hasThemePluginEnabled() ? trans('admin/plugin.enable_theme_modal.heading') : null) + ->modalDescription(fn (Plugin $plugin) => $plugin->isTheme() && Plugins::hasThemePluginEnabled() ? trans('admin/plugin.enable_theme_modal.description') : null) + ->action(function (Plugin $plugin, $livewire) { + Plugins::enablePlugin($plugin); - redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab])); + redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab])); - Notification::make() - ->success() - ->title(trans('admin/plugin.notifications.updated')) - ->send(); - }), - Action::make('disable') - ->label(trans('admin/plugin.disable')) - ->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin)) - ->icon('tabler-x') - ->color('danger') - ->visible(fn (Plugin $plugin) => $plugin->canDisable()) - ->action(function (Plugin $plugin, $livewire) { - Plugins::disablePlugin($plugin); + Notification::make() + ->success() + ->title(trans('admin/plugin.notifications.updated')) + ->send(); + }), + Action::make('disable') + ->label(trans('admin/plugin.disable')) + ->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin)) + ->icon('tabler-x') + ->color('warning') + ->visible(fn (Plugin $plugin) => $plugin->canDisable()) + ->action(function (Plugin $plugin, $livewire) { + Plugins::disablePlugin($plugin); - redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab])); + redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab])); - Notification::make() - ->success() - ->title(trans('admin/plugin.notifications.disabled')) - ->send(); - }), + Notification::make() + ->success() + ->title(trans('admin/plugin.notifications.disabled')) + ->send(); + }), + Action::make('delete') + ->label(trans('filament-actions::delete.single.label')) + ->authorize(fn (Plugin $plugin) => user()?->can('create', $plugin)) + ->icon('tabler-trash') + ->color('danger') + ->requiresConfirmation() + ->hidden(fn (Plugin $plugin) => $plugin->isInstalled()) + ->action(function (Plugin $plugin, $livewire) { + File::deleteDirectory(plugin_path($plugin->id)); + + redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab])); + + Notification::make() + ->success() + ->title(trans('admin/plugin.notifications.deleted')) + ->send(); + }), + Action::make('uninstall') + ->label(trans('admin/plugin.uninstall')) + ->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin)) + ->icon('tabler-terminal') + ->color('danger') + ->requiresConfirmation() + ->visible(fn (Plugin $plugin) => $plugin->isInstalled()) + ->action(function (Plugin $plugin, $livewire) { + Plugins::uninstallPlugin($plugin); + + redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab])); + + Notification::make() + ->success() + ->title(trans('admin/plugin.notifications.uninstalled')) + ->send(); + }), + ]), ]) ->headerActions([ Action::make('import') diff --git a/app/Services/Helpers/PluginService.php b/app/Services/Helpers/PluginService.php index 04cdbd0da..8f93be1f2 100644 --- a/app/Services/Helpers/PluginService.php +++ b/app/Services/Helpers/PluginService.php @@ -166,6 +166,22 @@ class PluginService } } + public function removeComposerPackages(Plugin $plugin): void + { + if ($plugin->composer_packages) { + $composerPackages = collect(json_decode($plugin->composer_packages, true, 512, JSON_THROW_ON_ERROR)) + ->map(fn ($version, $package) => "$package:$version") + ->flatten() + ->unique() + ->toArray(); + + $result = Process::path(base_path())->timeout(600)->run(['composer', 'remove', ...$composerPackages]); + if ($result->failed()) { + throw new Exception('Could not remove composer packages: ' . $result->errorOutput()); + } + } + } + public function runPluginMigrations(Plugin $plugin): void { $migrations = plugin_path($plugin->id, 'database', 'migrations'); @@ -178,6 +194,18 @@ class PluginService } } + public function rollbackPluginMigrations(Plugin $plugin): void + { + $migrations = plugin_path($plugin->id, 'database', 'migrations'); + if (file_exists($migrations)) { + $success = Artisan::call('migrate:rollback', ['--realpath' => true, '--path' => $migrations, '--force' => true]) === 0; + + if (!$success) { + throw new Exception("Could not rollback migrations for plugin '{$plugin->id}'"); + } + } + } + public function buildAssets(): bool { try { @@ -237,6 +265,25 @@ class PluginService } } + public function uninstallPlugin(Plugin $plugin, bool $deleteFiles = false): void + { + try { + $this->removeComposerPackages($plugin); + + $this->rollbackPluginMigrations($plugin); + + $this->buildAssets(); + + if ($deleteFiles) { + File::deleteDirectory(plugin_path($plugin->id)); + } else { + $this->setStatus($plugin, PluginStatus::NotInstalled); + } + } catch (Exception $exception) { + $this->handlePluginException($plugin, $exception); + } + } + public function downloadPluginFromFile(UploadedFile $file, bool $cleanDownload = false): void { // Validate file size to prevent zip bombs diff --git a/lang/en/admin/plugin.php b/lang/en/admin/plugin.php index e59e883de..ae5a4b0f8 100644 --- a/lang/en/admin/plugin.php +++ b/lang/en/admin/plugin.php @@ -14,6 +14,7 @@ return [ 'visit_website' => 'Visit Website', 'settings' => 'Settings', 'install' => 'Install', + 'uninstall' => 'Uninstall', 'update' => 'Update', 'enable' => 'Enable', 'disable' => 'Disable', @@ -46,6 +47,8 @@ return [ 'notifications' => [ 'installed' => 'Plugin installed', + 'uninstalled' => 'Plugin uninstalled', + 'deleted' => 'Plugin deleted', 'updated' => 'Plugin updated', 'enabled' => 'Plugin enabled', 'disabled' => 'Plugin disabled',