web/admin/pdf: gemeinsame Bildnachweis-Auflösung, Detailfenster am Artikelbild, Bestätigungsstand in Kunden- und Adminansicht, Wortwahl „automatisierte Prüfung" (Bildkennzeichnung Block D)

- App\Support\ImageCredit: Foto/Quelle-Zeile, KI-Hinweis mit Herkunft, Verantwortlich-Zeile nur mit Protokoll, sichere Links; PDF-Zeilen mit Bestätigungskennung
- Artikelseite: Overlay (eigenes i-Zeichen, Alpine ohne Plugin, Bottom-Sheet auf Mobil) mit Nachweis, Lizenz, Quelle, KI-Hinweis, Verantwortlich-Zeile, „Bild melden"
- Richtlinien 0.2: Prüfung als automatisiert beschrieben, Abschnitt „Bilder" (Decision-Update 3.5); Taglines, Suche, Newsrooms, Startseiten-CTA, Mails ohne „qualitätsgeprüft"
- Cache-Invalidierung bei Bildwechsel an veröffentlichten Meldungen; PHPStan-Baseline für das Resource bereinigt

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-22 09:44:34 +00:00
parent 65ac72ce05
commit 2124ffd1f2
26 changed files with 578 additions and 199 deletions

View file

@ -0,0 +1,141 @@
<?php
use App\Enums\ConfirmationChannel;
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\ImageRightsConfirmation;
use App\Models\LegalDocument;
use App\Models\PressRelease;
use App\Models\PressReleaseImage;
use App\Support\ImageCredit;
use Database\Seeders\LegalDocumentSeeder;
use Tests\TestCase;
/**
* Bildkennzeichnung Block D: eine Auflösung für Nachweiszeile, Overlay,
* Kunden-/Adminansicht und PDF; Verantwortlich-Zeile nur mit Protokoll.
*/
function creditPublishedRelease(): array
{
$category = Category::factory()->create(['portal' => Portal::Presseecho]);
$category->translations()->create(['locale' => 'de', 'name' => 'Energie', 'slug' => 'energie']);
$company = Company::factory()->presseecho()->create(['name' => 'Beispiel AG', 'is_active' => true]);
$release = PressRelease::factory()
->published()
->forPortal(Portal::Presseecho)
->for($category)
->for($company)
->create(['slug' => 'bild-nachweis-test', 'published_at' => now()->subDay()]);
return compact('category', 'company', 'release');
}
function creditConfirmationFor(PressReleaseImage $image, string $channel = 'form'): ImageRightsConfirmation
{
return 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::from($channel),
'legal_text_key' => 'img-confirm-v2',
'text_snapshot' => 'Ich bestätige …',
'agb_version' => '0.6',
'file_sha256' => $image->original_sha256,
'declaration_snapshot' => [],
'confirmed_at' => now()->setDate(2026, 9, 22)->setTime(10, 0),
'created_at' => now(),
]);
}
test('a formular v2 image renders credit line, ai hint with source and the responsible line', function () {
/** @var TestCase $this */
['release' => $release, 'company' => $company] = creditPublishedRelease();
$image = PressReleaseImage::factory()->for($release)->rulesV2()->create([
'copyright' => 'Anna Beispiel / Beispiel AG',
'ai_declaration' => 'ai_edited',
'ai_declaration_user' => 'ai_edited',
'ai_declaration_source' => 'customer',
'license_url' => 'https://example.com/lizenz',
'source_url' => 'javascript:alert(1)',
]);
creditConfirmationFor($image);
$credit = ImageCredit::for($image->fresh(), $company);
expect($credit->line())->toBe('Foto: Anna Beispiel / Beispiel AG · KI-bearbeitet')
->and($credit->responsibleLine)->toBe('Bild und Angaben: Beispiel AG, eingereicht am 22. September 2026')
->and($credit->aiSourceLabel)->toBe('Angabe des Einreichers')
->and($credit->licenseUrl)->toBe('https://example.com/lizenz')
->and($credit->sourceUrl)->toBeNull()
->and($credit->pdfLines())->toContain('Rechte bestätigt am 22.09.2026 10:00 (img-confirm-v2, AGB 0.6)');
$this->get('https://presseecho.test/de/press-release/bild-nachweis-test')
->assertSuccessful()
->assertSeeText('Foto: Anna Beispiel / Beispiel AG · KI-bearbeitet')
->assertSee('data-image-credit', false)
->assertSeeText('Dieses Bild wurde mit KI bearbeitet.')
->assertSeeText('Kennzeichnung: Angabe des Einreichers')
->assertSeeText('Bild und Angaben: Beispiel AG, eingereicht am 22. September 2026')
->assertSeeText('Bild melden')
->assertDontSee('javascript:alert', false);
});
test('a legacy image without credit falls back to the company as source and shows no responsible line', function () {
/** @var TestCase $this */
['release' => $release, 'company' => $company] = creditPublishedRelease();
$image = PressReleaseImage::factory()->for($release)->legacy()->create(['is_ai_generated' => true]);
$credit = ImageCredit::for($image, $company);
expect($credit->line())->toBe('Quelle: Beispiel AG · KI-generiert')
->and($credit->responsibleLine)->toBeNull()
->and($credit->aiSourceLabel)->toBeNull()
->and($credit->isLegacy)->toBeTrue();
$this->get('https://presseecho.test/de/press-release/bild-nachweis-test')
->assertSuccessful()
->assertSeeText('Quelle: Beispiel AG · KI-generiert')
->assertDontSeeText('Bild und Angaben');
});
test('a legacy image with author but without copyright uses the author and the old fields stay in the pdf lines', function () {
['release' => $release, 'company' => $company] = creditPublishedRelease();
$image = PressReleaseImage::factory()->for($release)->legacy()->create([
'author' => 'Max Mustermann',
'license_type' => 'own',
]);
$credit = ImageCredit::for($image, $company);
expect($credit->line())->toBe('Foto: Max Mustermann')
->and($credit->pdfLines())->toBe(['Bildnachweis: Max Mustermann']);
});
test('an image without any detail renders no line and no overlay', function () {
/** @var TestCase $this */
['release' => $release] = creditPublishedRelease();
PressReleaseImage::factory()->for($release)->legacy()->create();
$release->company()->update(['name' => '']);
$credit = ImageCredit::for($release->images()->first(), $release->company()->first());
expect($credit->line())->toBeNull()
->and($credit->hasDetails())->toBeFalse();
});
test('the guidelines page describes the automated check and the image rules under version 0.2', function () {
/** @var TestCase $this */
$this->seed(LegalDocumentSeeder::class);
expect(LegalDocument::current(LegalDocument::TYPE_RICHTLINIEN)?->version)->toBe('0.2');
$this->get('https://presseecho.test/de/richtlinien')
->assertSuccessful()
->assertSeeText('Jede Mitteilung durchläuft vor der Veröffentlichung eine automatisierte Prüfung.')
->assertSeeText('Zu jedem Bild gehört ein Bildnachweis')
->assertDontSeeText('geprüft, den Richtlinien entsprechend');
});