presseportale/app/Services/PressRelease/Images/ImageCorrectionService.php
Kevin Adametz b5b70416f2 archive: Beweisarchiv — verschlüsselte Einträge, Legal Hold, Purge mit Tombstone, Restore-Abgleich, Auslöser und Admin-Seite (Bildkennzeichnung Block F)
- archive_entries (append-only) + archive_access_log; Disk archive außerhalb des Web-Roots; eigener ARCHIVE_KEY (archive:key), AES-256-CBC, SHA-256 am Eintrag
- EvidenceArchive: record/open/manifest/export (signiertes ZIP), setLegalHold/releaseLegalHold (Frist läuft neu), purgeDue, exportTombstones, reconcileAfterRestore
- PressReleaseArchiver: image_removed (Original, Snapshot, Bestätigungen), pm_depublished (letzter veröffentlichter Stand), gdpr_change (Anonymisierung, Korrekturen), label_corrected
- Hooks in Remover, depublish/archive/changeStatusFromAdmin/deleteFromAdmin, AnonymizationService, CorrectionService, ImageCorrectionService
- Scheduler archive:purge 03:20 und archive:export-tombstones 03:30; archive:reconcile-after-restore im Runbook
- Admin ▸ Management ▸ Beweisarchiv (Liste, Legal Hold mit Pflichtvermerk, Öffnen/Export nur mit Zweck)
- Scope-Wächter: neue Befehle nutzen withoutGlobalScope(PortalScope::class)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 10:04:21 +00:00

89 lines
3.8 KiB
PHP

<?php
namespace App\Services\PressRelease\Images;
use App\Enums\AiDeclarationSource;
use App\Enums\AiImageDeclaration;
use App\Models\ImageDeclarationCorrection;
use App\Models\PressReleaseImage;
use App\Models\User;
use App\Services\Archive\PressReleaseArchiver;
use App\Support\WebCaches;
use Illuminate\Support\Facades\DB;
/**
* Korrekturweg (Bildkennzeichnung E.6, Decision-Update S17): Der Admin
* ändert Bildnachweis oder Portalkennzeichnung mit Pflichtgrund oder hält
* das Bild vorläufig zurück. Kundenangabe, ursprüngliche Bestätigung und
* Befund bleiben unverändert; jede Änderung ist insert-only protokolliert.
*/
class ImageCorrectionService
{
public function __construct(
private readonly PublishedImageSnapshot $snapshots,
private readonly PressReleaseArchiver $archiver,
) {}
public function correctCredit(PressReleaseImage $image, User $admin, string $newCredit, string $reason): ImageDeclarationCorrection
{
return $this->apply($image, $admin, ImageDeclarationCorrection::FIELD_CREDIT, $image->copyright, $newCredit, null, $reason, function () use ($image, $newCredit): void {
$image->forceFill(['copyright' => $newCredit])->save();
});
}
public function correctAiDeclaration(PressReleaseImage $image, User $admin, AiImageDeclaration $new, string $reason): ImageDeclarationCorrection
{
return $this->apply($image, $admin, ImageDeclarationCorrection::FIELD_AI_DECLARATION, $image->ai_declaration?->value, $new->value, $image->ai_declaration_source?->value, $reason, function () use ($image, $new): void {
$image->forceFill([
'ai_declaration' => $new,
'ai_declaration_source' => AiDeclarationSource::AdminCorrection,
'ai_declaration_locked' => false,
])->save();
});
}
public function withhold(PressReleaseImage $image, User $admin, string $reason): ImageDeclarationCorrection
{
return $this->apply($image, $admin, ImageDeclarationCorrection::FIELD_WITHHELD, '0', '1', null, $reason, function () use ($image): void {
$image->forceFill(['withheld_at' => now()])->save();
});
}
public function release(PressReleaseImage $image, User $admin, string $reason): ImageDeclarationCorrection
{
return $this->apply($image, $admin, ImageDeclarationCorrection::FIELD_WITHHELD, '1', '0', null, $reason, function () use ($image): void {
$image->forceFill(['withheld_at' => null])->save();
});
}
private function apply(PressReleaseImage $image, User $admin, string $field, ?string $old, ?string $new, ?string $oldSource, string $reason, \Closure $mutation): ImageDeclarationCorrection
{
$correction = DB::transaction(function () use ($image, $admin, $field, $old, $new, $oldSource, $reason, $mutation): ImageDeclarationCorrection {
$mutation();
return ImageDeclarationCorrection::query()->create([
'press_release_image_id' => $image->getKey(),
'press_release_id' => $image->press_release_id,
'admin_id' => $admin->getKey(),
'field' => $field,
'old_value' => $old,
'new_value' => $new,
'old_source' => $oldSource,
'reason' => $reason,
'created_at' => now(),
]);
});
// Öffentliche Fassung hat sich geändert: Snapshot nachziehen und
// Seiten-Caches verwerfen (D.5). Jede Korrektur zusätzlich als
// Archiveintrag label_corrected (Block F.4).
if ($image->wasEverPublished()) {
$this->snapshots->capture($image->fresh());
WebCaches::forgetPublicPages();
}
$this->archiver->labelCorrected($image->fresh(), $correction, $admin);
return $correction;
}
}