Add videos: library, block and section background
Videos join the media library as their own table (schema v28), with a video block and a looping background on sections. video.js starts loops only while visible, offers a pause button and leaves the still image for reduced motion and data saving. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PWYm8kjAWmZaB2QB2xuBhU
This commit is contained in:
1 parent
592e3d5342
commit
76613b9f8e
25 files changed
+674
-20
No files matched your search
@@ -24,6 +24,7 @@ use App\Content\StyleCompiler;
|
||||
use App\Content\StyleSchema;
|
||||
use App\Content\Theme;
|
||||
use App\Content\Turnstile;
|
||||
use App\Content\VideoLibrary;
|
||||
use App\Domain\InvalidDocument;
|
||||
use App\Domain\InvalidMenu;
|
||||
use App\Domain\InvalidPage;
|
||||
@@ -1399,6 +1400,95 @@ final class CmsTest extends TestCase
|
||||
self::assertStringContainsString('<script src="/assets/lightbox.js?v=', $html);
|
||||
}
|
||||
|
||||
public function testAVideoFromTheLibraryPlaysAsBlockAndAsSectionBackground(): void
|
||||
{
|
||||
// Umut: „Baust du video embedding gleich ein" - der Hero von kant-it.de soll ein Video
|
||||
// bekommen, ohne dass dafür Dateien von Hand auf den Server müssen.
|
||||
$videos = new VideoLibrary($this->db, $this->path.'-media');
|
||||
$file = tempnam(sys_get_temp_dir(), 'kant-video-');
|
||||
try {
|
||||
file_put_contents($file, '<html><script>alert(1)</script></html>');
|
||||
try {
|
||||
$videos->upload(new UploadedFile($file, 'film.mp4', test: true));
|
||||
self::fail('Eine Datei, die kein Video ist, darf nicht als Video durchgehen.');
|
||||
} catch (\InvalidArgumentException) {
|
||||
}
|
||||
file_put_contents($file, "\0\0\0\x14ftypqt \0\0\0\0qt ".str_repeat("\0", 64));
|
||||
try {
|
||||
$videos->upload(new UploadedFile($file, 'film.mov', test: true));
|
||||
self::fail('MOV spielen nicht alle Browser ab und wird deshalb abgewiesen.');
|
||||
} catch (\InvalidArgumentException) {
|
||||
}
|
||||
self::assertSame(0, (int) $this->db->fetchOne('SELECT COUNT(*) FROM video'));
|
||||
|
||||
file_put_contents($file, "\0\0\0\x18ftypmp42\0\0\0\0mp42isom".str_repeat('x', 2000));
|
||||
$item = $videos->upload(new UploadedFile($file, 'Hero Loop.mp4', test: true));
|
||||
file_put_contents($file, "\x1A\x45\xDF\xA3\x9F\x42\x86\x81\x01\x42\x82\x84webm".str_repeat('y', 2000));
|
||||
$webm = $videos->upload(new UploadedFile($file, 'hero.bin', test: true));
|
||||
} finally {
|
||||
@unlink($file);
|
||||
}
|
||||
self::assertSame('Hero Loop', $item['title']);
|
||||
self::assertSame('/media/'.$item['id'].'.mp4', $item['src']);
|
||||
self::assertSame('/media/'.$webm['id'].'.webm', $webm['src']);
|
||||
|
||||
$antwort = $this->request($item['src']);
|
||||
self::assertSame(200, $antwort->getStatusCode());
|
||||
self::assertSame('video/mp4', $antwort->headers->get('Content-Type'));
|
||||
self::assertSame('nosniff', $antwort->headers->get('X-Content-Type-Options'));
|
||||
self::assertSame("default-src 'none'", $antwort->headers->get('Content-Security-Policy'));
|
||||
self::assertSame('bytes', $antwort->headers->get('Accept-Ranges'));
|
||||
// Dieselbe Kennung unter der falschen Endung gibt es nicht.
|
||||
self::assertSame(404, $this->request('/media/'.$item['id'].'.webm')->getStatusCode());
|
||||
self::assertSame(404, $this->request('/media/'.str_repeat('a', 32).'.mp4')->getStatusCode());
|
||||
|
||||
$document = DemoContent::starter('Startseite');
|
||||
$document['schemaVersion'] = DocumentValidator::SCHEMA_VERSION;
|
||||
$document['blocks'][0]['props'] = ['backgroundVideo' => $item['src'], 'backgroundVideoWebm' => $webm['src']];
|
||||
$document['blocks'][] = ['id' => 'film', 'type' => 'video', 'props' => ['src' => $item['src'], 'mode' => 'player', 'title' => 'Unser Betrieb', 'poster' => '/media/'.str_repeat('b', 32).'/1600']];
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||||
$seite = $this->request('/');
|
||||
$html = $seite->getContent();
|
||||
self::assertStringContainsString('has-video', $html);
|
||||
self::assertMatchesRegularExpression('#<video class="section-video"[^>]* muted loop playsinline preload="none"[^>]*data-video-loop#', $html);
|
||||
// Das WebM steht zuerst: der Browser nimmt die erste Quelle, die er abspielen kann.
|
||||
self::assertLessThan(strpos($html, 'src="'.$item['src'].'"'), strpos($html, 'src="'.$webm['src'].'"'));
|
||||
self::assertStringContainsString('class="video-toggle"', $html);
|
||||
self::assertStringContainsString('aria-label="Unser Betrieb"', $html);
|
||||
self::assertMatchesRegularExpression('#<video class="video-media" controls playsinline preload="metadata" poster=#', $html);
|
||||
self::assertStringContainsString('<script type="module" src="/assets/video.js?v=', $html);
|
||||
self::assertStringContainsString("script-src 'self'", (string) $seite->headers->get('Content-Security-Policy'));
|
||||
|
||||
// Ein Spieler allein braucht kein Skript: die Seite bleibt bei script-src 'none'.
|
||||
$spieler = DemoContent::starter('Startseite');
|
||||
$spieler['schemaVersion'] = DocumentValidator::SCHEMA_VERSION;
|
||||
$spieler['blocks'][] = ['id' => 'film', 'type' => 'video', 'props' => ['src' => $item['src'], 'mode' => 'player', 'title' => '']];
|
||||
$version = $this->pages->findForEditor('page-1')['version'];
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $spieler, $version), $version + 1);
|
||||
$ohne = $this->request('/');
|
||||
self::assertStringNotContainsString('video.js', $ohne->getContent());
|
||||
self::assertStringContainsString("script-src 'none'", (string) $ohne->headers->get('Content-Security-Policy'));
|
||||
|
||||
// Fremde Adressen, zu altes Schema und unbekannte Betriebsarten weist das Dokument ab.
|
||||
$pruefer = new DocumentValidator();
|
||||
foreach ([
|
||||
['video', ['src' => 'https://example.com/film.mp4'], DocumentValidator::SCHEMA_VERSION],
|
||||
['video', ['src' => '/media/../secret.mp4'], DocumentValidator::SCHEMA_VERSION],
|
||||
['video', ['src' => $item['src'], 'mode' => 'autoplay'], DocumentValidator::SCHEMA_VERSION],
|
||||
['video', ['src' => $item['src']], 27],
|
||||
] as [$type, $props, $schema]) {
|
||||
$falsch = DemoContent::starter('Startseite');
|
||||
$falsch['schemaVersion'] = $schema;
|
||||
$falsch['blocks'][] = ['id' => 'film', 'type' => $type, 'props' => $props];
|
||||
try {
|
||||
$pruefer->validate($falsch);
|
||||
self::fail('Abgewiesen werden muss: '.json_encode($props));
|
||||
} catch (InvalidDocument) {
|
||||
$this->addToAssertionCount(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testAPdfFromTheLibraryIsALinkTargetAndOpensInTheViewer(): void
|
||||
{
|
||||
// Umut: „PDFs hochladen und als Button verlinken - und ein Inline-PDF-Viewer wie das
|
||||
|
||||
Reference in new issue
Block a user