diff --git a/app/Console/Commands/RetireLinkSpamCompanies.php b/app/Console/Commands/RetireLinkSpamCompanies.php new file mode 100644 index 00000000..ac203d1f --- /dev/null +++ b/app/Console/Commands/RetireLinkSpamCompanies.php @@ -0,0 +1,106 @@ +candidates(); + $count = (clone $query)->count(); + + if ($count === 0) { + $this->info('Keine Linkspam-Firmen gefunden.'); + + return self::SUCCESS; + } + + $this->table( + ['ID', 'Name', 'Angelegt', 'Adresse (Anfang)'], + (clone $query)->limit(10)->get()->map(fn (Company $company): array => [ + $company->id, + mb_strimwidth((string) $company->name, 0, 50, '…'), + $company->created_at?->format('d.m.Y'), + mb_strimwidth(str_replace(["\r", "\n"], ' ', (string) $company->address), 0, 60, '…'), + ])->all(), + ); + + if ($this->option('dry-run')) { + $this->info(sprintf('[Dry-Run] %d Firmen würden deaktiviert (10 Beispiele oben).', $count)); + + return self::SUCCESS; + } + + $ids = (clone $query)->pluck('id'); + $file = sprintf('companies-link-spam-%s.txt', now()->format('Y-m-d-His')); + Storage::disk('local')->put($file, $ids->implode("\n")); + + $retired = 0; + + foreach ($ids->chunk(500) as $chunk) { + $retired += Company::query() + ->withoutGlobalScope(PortalScope::class) + ->whereIn('id', $chunk->all()) + ->update(['is_active' => false]); + } + + $activityLog->record( + ActivityType::Correction, + sprintf('%d Legacy-Firmen mit Linkspam in den Stammdaten deaktiviert (IDs in storage/app/%s).', $retired, $file), + null, + null, + ['count' => $retired, 'file' => $file], + 'system', + ); + + $this->info(sprintf('%d Firmen deaktiviert. IDs: storage/app/%s', $retired, $file)); + + return self::SUCCESS; + } + + /** + * @return Builder + */ + private function candidates(): Builder + { + return Company::query() + ->withoutGlobalScope(PortalScope::class) + ->where('is_active', true) + ->whereNull('gone_at') + ->where(function (Builder $query): void { + foreach (['address', 'boilerplate', 'name'] as $column) { + foreach (self::MARKERS as $marker) { + $query->orWhere($column, 'like', '%'.$marker.'%'); + } + } + }) + ->whereDoesntHave('pressReleases', fn (Builder $q) => $q->withoutGlobalScope(PortalScope::class)->published()) + ->orderBy('id'); + } +} diff --git a/dev/2026-09-01/umsetzung-protokoll.md b/dev/2026-09-01/umsetzung-protokoll.md index c2e068cf..6eabdc02 100644 --- a/dev/2026-09-01/umsetzung-protokoll.md +++ b/dev/2026-09-01/umsetzung-protokoll.md @@ -1527,3 +1527,13 @@ wird der Host zum Namen („ak-autos.de"). Bestehende Website bleibt. Tests: `CompanyNameNormalizerTest` (11). Live: `companies:strip-links-from-names --dry-run`, Liste ansehen, dann ohne Flag. + +**bbcode-Leichen (Nachtrag, auf Wunsch):** `companies:retire-link-spam +{--dry-run}` deaktiviert aktive Firmen mit `[url=` oder `.txt`, ein ActivityLog-Eintrag mit Zahl. Vorher geprüft: +Die 1.302 davon mit Meldungen haben nur `rejected` (bis 2019) und +`archived` (bis 2022), nichts Öffentliches. Die 44 Besitzerkonten bleiben +unangetastet — 24 führen auch saubere Firmen. Dry-Run Dev-DB: 4.391. +Test: `RetireLinkSpamCompaniesTest`. diff --git a/tests/Feature/RetireLinkSpamCompaniesTest.php b/tests/Feature/RetireLinkSpamCompaniesTest.php new file mode 100644 index 00000000..81736c1f --- /dev/null +++ b/tests/Feature/RetireLinkSpamCompaniesTest.php @@ -0,0 +1,45 @@ +businessportal24()->create(['address' => 'bags']); + $bbcode = Company::factory()->presseecho()->create(['boilerplate' => '[b][url=http://www.copy.example]replica[/url][/b]']); + $publicWithSpam = Company::factory()->businessportal24()->create(['address' => '[url=http://www.x.example]x[/url]']); + PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(['company_id' => $publicWithSpam->id]); + $clean = Company::factory()->businessportal24()->create(['address' => 'Musterstraße 1, 12345 Musterstadt']); + + $this->artisan('companies:retire-link-spam', ['--dry-run' => true]) + ->expectsOutputToContain('[Dry-Run] 2 Firmen würden deaktiviert') + ->assertSuccessful(); + + expect($spam->fresh()->is_active)->toBeTrue(); + + $this->artisan('companies:retire-link-spam') + ->expectsOutputToContain('2 Firmen deaktiviert.') + ->assertSuccessful(); + + expect($spam->fresh()->is_active)->toBeFalse() + ->and($bbcode->fresh()->is_active)->toBeFalse() + ->and($publicWithSpam->fresh()->is_active)->toBeTrue() + ->and($clean->fresh()->is_active)->toBeTrue() + ->and($spam->fresh()->deleted_at)->toBeNull(); + + $files = Storage::disk('local')->files(); + expect($files)->toHaveCount(1) + ->and(Storage::disk('local')->get($files[0]))->toBe($spam->id."\n".$bbcode->id); + + // Zweiter Lauf findet nichts mehr. + $this->artisan('companies:retire-link-spam')->expectsOutputToContain('Keine Linkspam-Firmen gefunden.'); +});