- 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>
101 lines
4 KiB
PHP
101 lines
4 KiB
PHP
<?php
|
|
|
|
use App\Enums\ConfirmationChannel;
|
|
use App\Models\ImageRightsConfirmation;
|
|
use App\Models\PressRelease;
|
|
use App\Models\PressReleaseImage;
|
|
|
|
function makeConfirmationFor(PressReleaseImage $image): 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::Form,
|
|
'legal_text_key' => 'img-confirm-v2',
|
|
'text_snapshot' => 'Ich bestätige …',
|
|
'agb_version' => '0.6',
|
|
'file_sha256' => $image->original_sha256,
|
|
'declaration_snapshot' => ['copyright' => $image->copyright],
|
|
'ip_hmac' => ImageRightsConfirmation::hashIp('203.0.113.7'),
|
|
'user_agent' => 'PestBrowser/1.0',
|
|
'confirmed_at' => now(),
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
|
|
test('confirmations are insert-only', function () {
|
|
$image = PressReleaseImage::factory()->rulesV2()->create();
|
|
$confirmation = makeConfirmationFor($image);
|
|
|
|
expect(fn () => $confirmation->update(['agb_version' => '0.7']))->toThrow(LogicException::class)
|
|
->and(fn () => $confirmation->delete())->toThrow(LogicException::class)
|
|
->and(ImageRightsConfirmation::query()->count())->toBe(1);
|
|
});
|
|
|
|
test('only the purge for never published uploads may remove a confirmation', function () {
|
|
$image = PressReleaseImage::factory()->rulesV2()->create();
|
|
$confirmation = makeConfirmationFor($image);
|
|
|
|
$confirmation->purgeForUnpublished();
|
|
|
|
expect(ImageRightsConfirmation::query()->count())->toBe(0);
|
|
});
|
|
|
|
test('a hard deleted image leaves its confirmation with the snapshot in place', function () {
|
|
$image = PressReleaseImage::factory()->rulesV2()->create();
|
|
$confirmation = makeConfirmationFor($image);
|
|
$pressReleaseId = $image->press_release_id;
|
|
|
|
$image->forceDelete();
|
|
|
|
$confirmation->refresh();
|
|
|
|
expect($confirmation->subject_id)->toBe($image->getKey())
|
|
->and($confirmation->press_release_id)->toBe($pressReleaseId)
|
|
->and($confirmation->file_sha256)->toBe($image->original_sha256)
|
|
->and($confirmation->declaration_snapshot['copyright'])->toBe($image->copyright);
|
|
});
|
|
|
|
test('a hard deleted press release nulls the reference but keeps the row', function () {
|
|
$pressRelease = PressRelease::factory()->create();
|
|
$image = PressReleaseImage::factory()->for($pressRelease)->rulesV2()->create();
|
|
$confirmation = makeConfirmationFor($image);
|
|
|
|
$pressRelease->forceDelete();
|
|
|
|
expect($confirmation->fresh())->not->toBeNull()
|
|
->and($confirmation->fresh()?->press_release_id)->toBeNull();
|
|
});
|
|
|
|
test('the ip derivation is keyed, versioned and not a plain hash', function () {
|
|
config()->set('images.confirmation_ip_key', 'geheim-1');
|
|
$first = ImageRightsConfirmation::hashIp('203.0.113.7');
|
|
|
|
config()->set('images.confirmation_ip_key', 'geheim-2');
|
|
$second = ImageRightsConfirmation::hashIp('203.0.113.7');
|
|
|
|
expect($first)->toStartWith('v1:')
|
|
->and($first)->not->toBe($second)
|
|
->and($first)->not->toContain(hash('sha256', '203.0.113.7'))
|
|
->and(ImageRightsConfirmation::hashIp(null))->toBeNull();
|
|
});
|
|
|
|
test('the image exposes its latest valid confirmation bound to the current file hash', function () {
|
|
$image = PressReleaseImage::factory()->rulesV2()->create();
|
|
|
|
expect($image->hasValidRightsConfirmation())->toBeFalse()
|
|
->and($image->isLegacyRules())->toBeFalse();
|
|
|
|
makeConfirmationFor($image);
|
|
|
|
expect($image->fresh()->hasValidRightsConfirmation())->toBeTrue();
|
|
|
|
// Bildtausch: neuer Hash, alte Bestätigung zählt nicht mehr (F09).
|
|
$image->update(['original_sha256' => hash('sha256', 'neue-datei')]);
|
|
|
|
expect($image->fresh()->hasValidRightsConfirmation())->toBeFalse()
|
|
->and(PressReleaseImage::factory()->legacy()->create()->isLegacyRules())->toBeTrue();
|
|
});
|