Neue Artisan-Commands DeleteAllKasSubdomains und DeletePartnerShopSubdomains zum Aufraeumen von KAS-/Partnershop-Subdomains, jeweils mit Feature-Tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
214 lines
7.1 KiB
PHP
214 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Http\Controllers\Api\KasController;
|
|
use Exception;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class DeleteAllKasSubdomains extends Command
|
|
{
|
|
/**
|
|
* The name and signature of the console command.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $signature = 'subdomains:delete-all-kas
|
|
{--domain= : Optional nur Subdomains dieser Hauptdomain löschen}
|
|
{--limit= : Maximale Anzahl zu löschender Subdomains}
|
|
{--list-file= : Optionaler Pfad für die temporäre JSON-Liste}
|
|
{--force : Subdomains wirklich löschen; ohne diese Option läuft nur ein Dry-Run}';
|
|
|
|
/**
|
|
* The console command description.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $description = 'Löscht alle aktuell in KAS angelegten Subdomains anhand einer temporären KAS-Liste';
|
|
|
|
public function __construct(private KasController $kas)
|
|
{
|
|
parent::__construct();
|
|
}
|
|
|
|
/**
|
|
* Execute the console command.
|
|
*/
|
|
public function handle(): int
|
|
{
|
|
$domain = $this->option('domain') !== null
|
|
? trim((string) $this->option('domain'), '.')
|
|
: null;
|
|
$limit = $this->option('limit') !== null ? (int) $this->option('limit') : null;
|
|
$isForce = (bool) $this->option('force');
|
|
|
|
if ($domain === '') {
|
|
$this->error('Die Option --domain darf nicht leer sein.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
if ($limit !== null && $limit < 1) {
|
|
$this->error('Die Option --limit muss größer als 0 sein.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
try {
|
|
$candidates = $this->getKasSubdomains($domain);
|
|
|
|
if ($limit !== null) {
|
|
$candidates = $candidates->take($limit)->values();
|
|
}
|
|
|
|
$listFilePath = $this->writeTemporaryList($candidates, $domain, $isForce);
|
|
$this->printPreview($candidates, $domain, $listFilePath, $isForce);
|
|
|
|
if ($candidates->isEmpty()) {
|
|
$this->info('Keine KAS-Subdomains gefunden.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
if (! $isForce) {
|
|
$this->warn('Dry-Run: Es wurde nichts gelöscht. Mit --force werden die Subdomains aus der temporären Liste gelöscht.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
return $this->deleteSubdomains($candidates);
|
|
} catch (Exception $e) {
|
|
$this->error('Fehler beim Löschen der KAS-Subdomains: '.$e->getMessage());
|
|
Log::error('KAS Komplett-Subdomain-Löschung fehlgeschlagen', [
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
]);
|
|
|
|
return self::FAILURE;
|
|
}
|
|
}
|
|
|
|
private function getKasSubdomains(?string $domain): Collection
|
|
{
|
|
$this->info('Rufe Subdomains direkt von KAS ab...');
|
|
|
|
$subdomains = $this->kas->action('get_subdomains');
|
|
|
|
if (! is_iterable($subdomains)) {
|
|
throw new Exception('KAS hat keine auswertbare Subdomain-Liste zurückgegeben: '.json_encode($subdomains));
|
|
}
|
|
|
|
return collect($subdomains)
|
|
->filter(function (array $subdomain) use ($domain): bool {
|
|
$name = strtolower((string) ($subdomain['subdomain_name'] ?? ''));
|
|
|
|
if ($name === '') {
|
|
return false;
|
|
}
|
|
|
|
return $domain === null || str_ends_with($name, '.'.strtolower($domain));
|
|
})
|
|
->map(fn (array $subdomain): array => [
|
|
'subdomain_name' => (string) $subdomain['subdomain_name'],
|
|
'subdomain_path' => (string) ($subdomain['subdomain_path'] ?? ''),
|
|
'php_version' => (string) ($subdomain['php_version'] ?? ''),
|
|
'is_active' => (string) ($subdomain['is_active'] ?? ''),
|
|
])
|
|
->unique('subdomain_name')
|
|
->sortBy('subdomain_name')
|
|
->values();
|
|
}
|
|
|
|
private function writeTemporaryList(Collection $candidates, ?string $domain, bool $isForce): string
|
|
{
|
|
$listFilePath = $this->resolveListFilePath();
|
|
File::ensureDirectoryExists(dirname($listFilePath));
|
|
|
|
File::put($listFilePath, json_encode([
|
|
'generated_at' => now()->toIso8601String(),
|
|
'mode' => $isForce ? 'delete' : 'dry-run',
|
|
'domain_filter' => $domain,
|
|
'count' => $candidates->count(),
|
|
'subdomains' => $candidates->values()->all(),
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
|
|
return $listFilePath;
|
|
}
|
|
|
|
private function resolveListFilePath(): string
|
|
{
|
|
$listFilePath = (string) ($this->option('list-file') ?? '');
|
|
|
|
if ($listFilePath !== '') {
|
|
return str_starts_with($listFilePath, '/') ? $listFilePath : base_path($listFilePath);
|
|
}
|
|
|
|
return storage_path('app/kas-subdomain-delete-lists/kas-subdomains-'.now()->format('Ymd-His').'.json');
|
|
}
|
|
|
|
private function printPreview(Collection $candidates, ?string $domain, string $listFilePath, bool $isForce): void
|
|
{
|
|
$this->newLine();
|
|
$this->info('Modus: '.($isForce ? 'Löschen' : 'Dry-Run'));
|
|
$this->info('Domain-Filter: '.($domain ?: 'keiner'));
|
|
$this->info('Temporäre Liste: '.$listFilePath);
|
|
$this->info('Gefundene KAS-Subdomains: '.$candidates->count());
|
|
|
|
if ($candidates->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
$this->table(
|
|
['Subdomain', 'Pfad', 'PHP', 'Aktiv'],
|
|
$candidates->take(25)->map(fn (array $candidate): array => [
|
|
$candidate['subdomain_name'],
|
|
$candidate['subdomain_path'] ?: '-',
|
|
$candidate['php_version'] ?: '-',
|
|
$candidate['is_active'] ?: '-',
|
|
])->all()
|
|
);
|
|
|
|
if ($candidates->count() > 25) {
|
|
$this->line('... weitere '.($candidates->count() - 25).' Subdomains ausgeblendet.');
|
|
}
|
|
}
|
|
|
|
private function deleteSubdomains(Collection $candidates): int
|
|
{
|
|
$deleted = 0;
|
|
$failed = 0;
|
|
|
|
foreach ($candidates as $candidate) {
|
|
$subdomainName = $candidate['subdomain_name'];
|
|
$result = $this->kas->action('delete_subdomain', [
|
|
'subdomain_name' => $subdomainName,
|
|
]);
|
|
|
|
if ($this->isSuccessfulDeleteResult($result, $subdomainName)) {
|
|
$deleted++;
|
|
$this->info("Gelöscht: {$subdomainName}");
|
|
|
|
continue;
|
|
}
|
|
|
|
$failed++;
|
|
$this->error("Fehler beim Löschen von {$subdomainName}: ".json_encode($result));
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info("Gelöscht: {$deleted}");
|
|
$this->info("Fehler: {$failed}");
|
|
|
|
return $failed === 0 ? self::SUCCESS : self::FAILURE;
|
|
}
|
|
|
|
private function isSuccessfulDeleteResult(mixed $result, string $subdomainName): bool
|
|
{
|
|
return $result === true
|
|
|| $result === 'TRUE'
|
|
|| $result === $subdomainName;
|
|
}
|
|
}
|