- legal_document_acceptances (insert-only), LegalAcceptanceGate, Pflicht-Schalter im Einreichungs-Modal über den gemeinsamen Trait; API: GET/POST /api/v1/legal/acceptances, submit antwortet 428 mit den geforderten Fassungen; LEGAL_ACCEPTANCE_GATE - ImageService: og-Variante (1200×630) mit halbtransparentem Balken und Inter SemiBold, regenerateOgVariant() für Korrekturen und Bestand; Fokuspunkt/Letterbox getestet - Uploader brennt den Nachweis beim Upload ein, ImageCorrectionService baut die OG-Variante nach einer Korrektur neu; Artikelseite nutzt og für og:image - Teaser: Nachweis in alt/title (Feed, Hero, Kategorie, Newsroom), Unterschrift am Hero-Bild; PressRelease::cardImageCredit() Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
179 lines
5.5 KiB
PHP
179 lines
5.5 KiB
PHP
<?php
|
||
|
||
namespace App\Support;
|
||
|
||
use App\Enums\AiDeclarationSource;
|
||
use App\Models\Company;
|
||
use App\Models\ImageRightsConfirmation;
|
||
use App\Models\PressReleaseImage;
|
||
use Illuminate\Support\Str;
|
||
|
||
/**
|
||
* Eine Auflösung des Bildnachweises für alle Ausgaben (Bildkennzeichnung
|
||
* D.1): Artikelseite, Kunden- und Adminansicht, Beleg-PDF, Overlay.
|
||
*
|
||
* Regeln (Decision-Update 3.4):
|
||
* - Nachweiszeile: „Foto: [copyright]", sonst „Foto: [author]", sonst im
|
||
* Bestand „Quelle: [Firma]".
|
||
* - KI-Zusatz aus der Portalkennzeichnung, Bestand-Fallback `is_ai_generated`.
|
||
* - Verantwortlich-Zeile „Bild und Angaben: [Firma], eingereicht am [Datum]"
|
||
* NUR mit Bestätigungsprotokoll — der Altbestand hat keins.
|
||
* - Links nur mit http/https.
|
||
*/
|
||
final readonly class ImageCredit
|
||
{
|
||
private function __construct(
|
||
public ?string $credit,
|
||
public string $creditLabel,
|
||
public ?string $aiLabel,
|
||
public ?string $aiSourceLabel,
|
||
public ?string $licenseUrl,
|
||
public ?string $sourceUrl,
|
||
public ?string $responsibleLine,
|
||
public ?ImageRightsConfirmation $confirmation,
|
||
public bool $isLegacy,
|
||
) {}
|
||
|
||
public static function for(PressReleaseImage $image, ?Company $company = null): self
|
||
{
|
||
$company ??= $image->pressRelease?->company;
|
||
$companyName = $company?->name;
|
||
|
||
$credit = self::clean($image->copyright) ?? self::clean($image->author);
|
||
$creditLabel = __('web.detail.photo');
|
||
|
||
if ($credit === null && $image->isLegacyRules() && filled($companyName)) {
|
||
$credit = $companyName;
|
||
$creditLabel = __('web.detail.source');
|
||
}
|
||
|
||
$aiLabel = $image->ai_declaration?->publicLabel() ?? ($image->is_ai_generated ? __('KI-generiert') : null);
|
||
|
||
$confirmation = $image->relationLoaded('latestRightsConfirmation')
|
||
? $image->latestRightsConfirmation
|
||
: $image->latestRightsConfirmation()->first();
|
||
|
||
$responsible = null;
|
||
|
||
if ($confirmation !== null && filled($companyName)) {
|
||
$responsible = __('web.detail.image_responsible', [
|
||
'company' => $companyName,
|
||
'date' => $confirmation->confirmed_at->locale('de')->isoFormat('D. MMMM YYYY'),
|
||
]);
|
||
}
|
||
|
||
return new self(
|
||
credit: $credit,
|
||
creditLabel: $creditLabel,
|
||
aiLabel: $aiLabel,
|
||
aiSourceLabel: $aiLabel !== null ? $image->ai_declaration_source?->publicLabel() ?? ($image->isLegacyRules() ? null : AiDeclarationSource::Customer->publicLabel()) : null,
|
||
licenseUrl: self::safeUrl($image->license_url),
|
||
sourceUrl: self::safeUrl($image->source_url),
|
||
responsibleLine: $responsible,
|
||
confirmation: $confirmation,
|
||
isLegacy: $image->isLegacyRules(),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Zeile aus Einzelteilen — für den eingebrannten OG-Balken beim Upload,
|
||
* bevor es ein Modell gibt (H.1).
|
||
*/
|
||
public static function compose(?string $credit, ?string $aiLabel, ?string $label = null): ?string
|
||
{
|
||
$credit = self::clean($credit);
|
||
$parts = [];
|
||
|
||
if ($credit !== null) {
|
||
$parts[] = ($label ?? __('web.detail.photo')).': '.$credit;
|
||
}
|
||
|
||
if ($aiLabel !== null && $aiLabel !== '') {
|
||
$parts[] = $aiLabel;
|
||
}
|
||
|
||
return $parts === [] ? null : implode(' · ', $parts);
|
||
}
|
||
|
||
/**
|
||
* Sichtbare Zeile unter dem Bild, z. B. „Foto: Musterfirma · KI-generiert".
|
||
*/
|
||
public function line(): ?string
|
||
{
|
||
if ($this->credit === null && $this->aiLabel === null) {
|
||
return null;
|
||
}
|
||
|
||
$parts = [];
|
||
|
||
if ($this->credit !== null) {
|
||
$parts[] = $this->creditLabel.': '.$this->credit;
|
||
}
|
||
|
||
if ($this->aiLabel !== null) {
|
||
$parts[] = $this->aiLabel;
|
||
}
|
||
|
||
return implode(' · ', $parts);
|
||
}
|
||
|
||
public function hasDetails(): bool
|
||
{
|
||
return $this->credit !== null || $this->aiLabel !== null || $this->responsibleLine !== null;
|
||
}
|
||
|
||
/**
|
||
* Zeilen für das Beleg-PDF (D.4).
|
||
*
|
||
* @return list<string>
|
||
*/
|
||
public function pdfLines(): array
|
||
{
|
||
$lines = [];
|
||
|
||
if ($this->credit !== null) {
|
||
$lines[] = 'Bildnachweis: '.$this->credit;
|
||
}
|
||
|
||
if ($this->licenseUrl !== null) {
|
||
$lines[] = 'Lizenz: '.$this->licenseUrl;
|
||
}
|
||
|
||
if ($this->sourceUrl !== null) {
|
||
$lines[] = 'Quelle: '.$this->sourceUrl;
|
||
}
|
||
|
||
if ($this->aiLabel !== null) {
|
||
$lines[] = 'KI-Kennzeichnung: '.$this->aiLabel.($this->aiSourceLabel !== null ? ' ('.$this->aiSourceLabel.')' : '');
|
||
}
|
||
|
||
if ($this->confirmation !== null) {
|
||
$lines[] = sprintf(
|
||
'Rechte bestätigt am %s (%s, AGB %s)',
|
||
$this->confirmation->confirmed_at->format('d.m.Y H:i'),
|
||
$this->confirmation->legal_text_key,
|
||
$this->confirmation->agb_version ?? '–',
|
||
);
|
||
}
|
||
|
||
return $lines;
|
||
}
|
||
|
||
private static function clean(?string $value): ?string
|
||
{
|
||
$value = trim((string) $value);
|
||
|
||
return $value !== '' ? $value : null;
|
||
}
|
||
|
||
private static function safeUrl(?string $url): ?string
|
||
{
|
||
$url = trim((string) $url);
|
||
|
||
if ($url === '' || ! Str::startsWith(strtolower($url), ['http://', 'https://'])) {
|
||
return null;
|
||
}
|
||
|
||
return filter_var($url, FILTER_VALIDATE_URL) !== false ? $url : null;
|
||
}
|
||
}
|