presseportale/app/Console/Commands/BackfillImageRightsConfirmations.php
Kevin Adametz 214aea8910 images: Datenmodell, Textarchiv, Herkunftsprüfung und gemeinsamer Uploader für Formular v2 (Bildkennzeichnung Block A)
- press_release_images: Kundenangabe/Portalkennzeichnung getrennt, Herkunftsbefund, Beweisoriginal, Snapshot-Pfade, published_ever_at, rules_version, Fokus/Letterbox; Bestand unangetastet
- legal_texts (insert-only) mit img-confirm-v1 (alter Wortlaut), v2 (AGB 0.6), v3 (AGB 0.7, noch ohne Datum); image-rights:backfill-confirmations
- image_rights_confirmations polymorph, insert-only, IP als versionierter HMAC; Löschung nur für nie veröffentlichte Uploads
- ImageProvenanceInspector (Imagick/exif): IPTC, XMP, EXIF; algorithmicallyEnhanced nur Vermerk; unlesbare Datei = error
- PressReleaseImageUploader: Original vor Derivaten, Hash-Bindung, Titelbild-Regel unter Sperre, Dateien bei Fehlern zurückgerollt; Admin-Upload ohne Bestätigung
- ImageService: kein Rückfall auf die Quelldatei, og-Variante, Fokuspunkt und no_crop

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

84 lines
3.1 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Enums\ConfirmationChannel;
use App\Models\ImageRightsConfirmation;
use App\Models\LegalText;
use App\Models\PressReleaseImage;
use Illuminate\Console\Command;
/**
* Bildkennzeichnung A.3: Die Bestätigungen des alten Formulars
* (`rights_confirmed_at`, ohne Kennung) bekommen rückwirkend je eine Zeile
* im Protokoll mit Verweis auf `img-confirm-v1`. Ohne IP und User-Agent —
* die wurden damals nicht erfasst. Idempotent.
*/
class BackfillImageRightsConfirmations extends Command
{
protected $signature = 'image-rights:backfill-confirmations {--dry-run : Nur zählen, nichts schreiben}';
protected $description = 'Legt für Bilder des alten Formulars rückwirkend einen Protokolleintrag mit img-confirm-v1 an';
public function handle(): int
{
$text = LegalText::findByKey('img-confirm-v1');
if ($text === null) {
$this->error('img-confirm-v1 fehlt — zuerst LegalTextSeeder ausführen.');
return self::FAILURE;
}
$query = PressReleaseImage::query()
->withTrashed()
->whereNotNull('rights_confirmed_at')
->whereDoesntHave('rightsConfirmations')
->with('pressRelease');
$count = $query->count();
$this->info("Bilder mit alter Bestätigung ohne Protokoll: {$count}");
if ($this->option('dry-run') || $count === 0) {
return self::SUCCESS;
}
$written = 0;
$query->chunkById(200, function ($images) use ($text, &$written): void {
foreach ($images as $image) {
ImageRightsConfirmation::query()->create([
'subject_type' => $image->getMorphClass(),
'subject_id' => $image->getKey(),
'press_release_id' => $image->press_release_id,
'company_id' => $image->pressRelease?->company_id,
'user_id' => $image->pressRelease?->user_id,
'channel' => ConfirmationChannel::Form,
'legal_text_id' => $text->id,
'legal_text_key' => $text->key,
'text_snapshot' => $text->body,
'agb_version' => $text->bound_agb_version,
'locale' => $text->locale,
'file_sha256' => null,
'declaration_snapshot' => [
'copyright' => $image->copyright,
'author' => $image->author,
'license_type' => $image->license_type?->value,
'license_detail' => $image->license_detail,
'license_url' => $image->license_url,
'source_url' => $image->source_url,
'ai_declaration' => $image->ai_declaration?->value,
'backfilled' => true,
],
'confirmed_at' => $image->rights_confirmed_at,
'created_at' => now(),
]);
$written++;
}
});
$this->info("Protokolleinträge angelegt: {$written}");
return self::SUCCESS;
}
}