- 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>
89 lines
3.8 KiB
PHP
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;
|
|
}
|
|
}
|