107 lines
7.7 KiB
PHP
107 lines
7.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
// AI disclosure labels: storage, sync along file operations, detection and markup.
|
|
|
|
$root = dirname(__DIR__);
|
|
$tmp = sys_get_temp_dir() . '/raiju-ai-labels-' . bin2hex(random_bytes(4));
|
|
mkdir($tmp, 0755, true);
|
|
define('RAIJU_MEDIA_META_PATH', $tmp . '/media-meta.json');
|
|
define('SETTINGS_PATH', $tmp . '/settings.json');
|
|
require_once $root . '/includes/ai-labels.php';
|
|
|
|
function aiLabelAssert(bool $condition, string $message): void {
|
|
if (!$condition) {
|
|
fwrite(STDERR, "FAIL: {$message}\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Public sources map to media type and relative name
|
|
aiLabelAssert(raijuMediaRefFromSrc('/assets/images/a.webp') === ['type' => 'image', 'name' => 'a.webp'], 'root-relative image path');
|
|
aiLabelAssert(raijuMediaRefFromSrc('../assets/images/team/b.jpg?v=3') === ['type' => 'image', 'name' => 'team/b.jpg'], 'relative path with folder and query');
|
|
aiLabelAssert(raijuMediaRefFromSrc('https://example.org/site/assets/videos/c.mp4') === ['type' => 'video', 'name' => 'c.mp4'], 'absolute video URL');
|
|
aiLabelAssert(raijuMediaRefFromSrc('/assets/images/../admin/x.php') === null, 'path traversal must be rejected');
|
|
aiLabelAssert(raijuMediaRefFromSrc('/css/style.css') === null, 'non-media sources carry no label');
|
|
|
|
// Set, read and remove
|
|
aiLabelAssert(raijuMediaAiLabel('/assets/images/a.webp') === '', 'unlabelled file');
|
|
aiLabelAssert(raijuSetMediaAiLabel('image', 'a.webp', 'generated'), 'label must be stored');
|
|
aiLabelAssert(raijuMediaAiLabel('assets/images/a.webp') === 'generated', 'stored label must be read back');
|
|
aiLabelAssert(raijuSetMediaAiLabel('image', 'a.webp', 'invalid') && raijuMediaAiLabel('/assets/images/a.webp') === '', 'unknown kinds remove the label');
|
|
aiLabelAssert(raijuSetMediaAiLabel('image', 'a.webp', ''), 'removing a missing label must succeed');
|
|
|
|
// Rename/move, trash, restore, permanent delete
|
|
raijuSetMediaAiLabel('image', 'b.webp', 'modified');
|
|
aiLabelAssert(raijuMoveMediaMeta('image', 'b.webp', 'image', 'fotos/b.webp') && raijuMediaAiLabelFor('image', 'fotos/b.webp') === 'modified', 'label must follow a move');
|
|
aiLabelAssert(raijuMediaAiLabelFor('image', 'b.webp') === '', 'old name must no longer carry the label');
|
|
raijuMoveMediaMeta('image', 'fotos/b.webp', 'image-trash', 'fotos/b.webp');
|
|
aiLabelAssert(raijuMediaAiLabelFor('image-trash', 'fotos/b.webp') === 'modified', 'label must go to the trash namespace');
|
|
raijuMoveMediaMeta('image-trash', 'fotos/b.webp', 'image', 'fotos/b-1.webp');
|
|
aiLabelAssert(raijuMediaAiLabelFor('image', 'fotos/b-1.webp') === 'modified', 'label must come back on restore');
|
|
raijuMoveMediaMeta('image', 'fotos/b-1.webp', 'image-trash', 'fotos/b-1.webp');
|
|
aiLabelAssert(raijuForgetMediaMeta('image-trash', 'fotos/b-1.webp') && raijuMediaAiLabelFor('image-trash', 'fotos/b-1.webp') === '', 'permanent delete must forget the label');
|
|
$stored = json_decode((string)file_get_contents(RAIJU_MEDIA_META_PATH), true);
|
|
aiLabelAssert($stored === [], 'empty namespaces must be pruned');
|
|
|
|
// Detection from embedded provenance (IPTC digital source type)
|
|
$generated = $tmp . '/generated.jpg';
|
|
file_put_contents($generated, "xmp Iptc4xmpExt:DigitalSourceType=\"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia\"");
|
|
$modified = $tmp . '/modified.png';
|
|
file_put_contents($modified, "c2pa digitalSourceType http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia");
|
|
$plain = $tmp . '/plain.webp';
|
|
file_put_contents($plain, 'RIFF....WEBPVP8 ');
|
|
aiLabelAssert(raijuDetectAiLabel($generated) === 'generated', 'trained algorithmic media must be detected as generated');
|
|
aiLabelAssert(raijuDetectAiLabel($modified) === 'modified', 'composites must be detected as modified');
|
|
aiLabelAssert(raijuDetectAiLabel($plain) === '', 'files without provenance carry no label');
|
|
|
|
// Markup: English artwork by default, alt text in the page language
|
|
$GLOBALS['basePath'] = '../';
|
|
raijuSetMediaAiLabel('image', 'c.webp', 'generated');
|
|
aiLabelAssert(raijuAiLabelArtworkMode() === 'en', 'English artwork is the default');
|
|
$de = raijuAiLabelHtml('/assets/images/c.webp', 'de');
|
|
aiLabelAssert(str_contains($de, 'class="rj-ai-label rj-ai-label--generated"'), 'label wrapper with kind modifier');
|
|
aiLabelAssert(str_contains($de, 'src="../css/ai-labels/en-generated.svg"') && str_contains($de, 'alt="KI-generiert"'), 'English artwork with a German alt text on German pages');
|
|
$fr = raijuAiLabelHtml('/assets/images/c.webp', 'fr');
|
|
aiLabelAssert(str_contains($fr, 'en-generated.svg') && str_contains($fr, 'alt="Généré par IA"'), 'English artwork with a localized alt text');
|
|
|
|
// Optional German artwork on German pages (Settings → Language → AI labels on images)
|
|
file_put_contents(SETTINGS_PATH, json_encode(['general' => ['aiLabelArtwork' => 'page']]));
|
|
aiLabelAssert(raijuAiLabelArtworkMode(true) === 'page', 'the page-language artwork can be selected');
|
|
aiLabelAssert(str_contains(raijuAiLabelHtml('/assets/images/c.webp', 'de'), 'src="../css/ai-labels/de-generated.svg"'), 'German artwork on German pages when selected');
|
|
aiLabelAssert(str_contains(raijuAiLabelHtml('/assets/images/c.webp', 'fr'), 'en-generated.svg'), 'other languages keep the English artwork');
|
|
file_put_contents(SETTINGS_PATH, json_encode(['general' => ['aiLabelArtwork' => 'unknown']]));
|
|
aiLabelAssert(raijuAiLabelArtworkMode(true) === 'en', 'unknown artwork modes fall back to English');
|
|
aiLabelAssert(raijuAiLabelHtml('/assets/images/unlabelled.webp', 'de') === '', 'unlabelled images render no label');
|
|
aiLabelAssert(raijuAiLabelsRendered(), 'rendered labels must be tracked for the touch script');
|
|
$framed = raijuAiLabelAttach('<img src="/assets/images/c.webp" alt="">', '/assets/images/c.webp', true);
|
|
aiLabelAssert(str_starts_with($framed, '<span class="rj-ai-media"><img') && str_ends_with($framed, '</span></span>'), 'framed image and label');
|
|
aiLabelAssert(raijuAiLabelFrame('<img src="x.webp" alt="">') === '<img src="x.webp" alt="">', 'unlabelled markup stays unframed');
|
|
|
|
// Editor data: labelled files and label templates, without marking labels as rendered
|
|
unset($GLOBALS['raijuAiLabelsRendered']);
|
|
raijuSetMediaAiLabel('video', 'clip.mp4', 'modified');
|
|
$editorJson = json_encode(raijuAiLabelEditorConfig('de'));
|
|
$editor = json_decode((string)$editorJson, true);
|
|
aiLabelAssert(($editor['files']['image']['c.webp'] ?? '') === 'generated' && ($editor['files']['video']['clip.mp4'] ?? '') === 'modified', 'editor data lists labelled files');
|
|
aiLabelAssert(str_contains($editor['markup']['generated'] ?? '', 'en-generated.svg') && str_contains($editor['markup']['modified'] ?? '', 'rj-ai-label--modified'), 'editor data carries the label markup per kind');
|
|
aiLabelAssert(!raijuAiLabelsRendered(), 'label templates for the editor do not count as rendered labels');
|
|
raijuSetMediaAiLabel('image', 'c.webp', '');
|
|
raijuSetMediaAiLabel('video', 'clip.mp4', '');
|
|
aiLabelAssert(str_starts_with((string)json_encode(raijuAiLabelEditorConfig('de')), '{"files":{}'), 'no labelled files encode as an object');
|
|
|
|
// en-ai is a round "AI" badge that ships for later use (e.g. AI-generated texts)
|
|
foreach (['de-generated', 'de-modified', 'en-generated', 'en-modified', 'en-ai'] as $artwork) {
|
|
aiLabelAssert(is_file($root . '/css/ai-labels/' . $artwork . '.svg'), "artwork {$artwork} must ship with the core");
|
|
}
|
|
} finally {
|
|
array_map('unlink', glob($tmp . '/*') ?: []);
|
|
array_map('unlink', glob($tmp . '/.*.lock') ?: []);
|
|
@unlink(RAIJU_MEDIA_META_PATH . '.lock');
|
|
@rmdir($tmp);
|
|
}
|
|
|
|
echo "AI label smoke test passed.\n";
|