| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/arni-solutions.de/web/terminal/ |
Upload File : |
<?php
/**
* Pinnwand – Medien-Auslieferung mit Terminal-Auth + HTTP-Range (Video-Streaming).
*
* GET pinnwand-media.php?v=<view_token>&term=<hash>&f=<base>&s=t|m|o|p|v|l|a|h|s|ls
* s=t Thumbnail s=m Feed-Größe s=o Original(JPEG) s=p Video-Poster
* s=v Videodatei (mp4/webm, mit Range) s=l Link-Vorschaubild
* s=a Profilfoto (klein) s=h Profil-Header
* s=s Mini (160px, Kachel-/Kommentar-Thumbs; Bild oder Video-Poster)
* s=ls Mini des Link-Vorschaubilds
* Mini-Größen werden für Bestandsdateien bei der ersten Anfrage
* aus _t/_p bzw. dem Link-Bild erzeugt und gecacht (Lazy-Konvertierung).
*
* Auth identisch zu view.php: gültiger Unlock-Cookie + Ansicht zugewiesen/global.
* Dateien liegen unter data/pinnwand/<view_id>/ – Dateiname ist ein 32-Hex-Token,
* es wird NIE ein Nutzer-Pfad verwendet (kein Traversal möglich).
*/
require __DIR__ . '/config.inc.php';
$viewToken = (string)($_GET['v'] ?? '');
$termHash = (string)($_GET['term'] ?? '');
$base = (string)($_GET['f'] ?? '');
$size = (string)($_GET['s'] ?? 'm');
if (!preg_match('/^[a-f0-9]{32}$/', $base) || !in_array($size, ['t', 'm', 'o', 'p', 'v', 'l', 'a', 'h', 's', 'ls'], true)) {
http_response_code(404); exit;
}
/** Mini-Variante (max. 160px) aus einer JPEG-/PNG-Quelle erzeugen (PNG behält den Alphakanal). */
function term_media_mini(string $src, string $dst, int $max = 160): bool
{
if (!is_file($src) || !function_exists('imagecreatefromstring')) return false;
$img = @imagecreatefromstring((string)@file_get_contents($src));
if (!$img) return false;
$iw = imagesx($img); $ih = imagesy($img);
$sc = min(1, $max / max($iw, $ih));
$nw = max(1, (int)round($iw * $sc)); $nh = max(1, (int)round($ih * $sc));
$out = imagecreatetruecolor($nw, $nh);
$png = substr($dst, -4) === '.png';
if ($png) { imagealphablending($out, false); imagesavealpha($out, true); }
imagecopyresampled($out, $img, 0, 0, 0, 0, $nw, $nh, $iw, $ih);
$ok = $png ? @imagepng($out, $dst, 6) : @imagejpeg($out, $dst, 75);
imagedestroy($out);
imagedestroy($img);
return (bool)$ok;
}
$db = term_db();
$terminal = term_terminal_grants_for($db, $viewToken, $termHash);
if (!$terminal) { http_response_code(403); exit; }
$st = $db->prepare('SELECT id FROM ' . term_tbl('views') . ' WHERE view_token = ? AND view_active = 1');
$st->execute([$viewToken]);
$wid = (int)$st->fetchColumn();
if (!$wid) { http_response_code(404); exit; }
$dir = dirname(__FILE__) . '/data/pinnwand/' . $wid;
if ($size === 'l') {
// Link-Vorschaubild (Cache-Verzeichnis „links", Feed-Größe)
$path = "$dir/links/{$base}_m.jpg";
if (!is_file($path)) $path = "$dir/links/$base.jpg";
$mime = 'image/jpeg';
} elseif ($size === 's' || $size === 'ls') {
// Mini (160px): für Bestandsdateien bei der ersten Anfrage erzeugen.
// Quelle: Bild-Thumbnail _t (JPEG oder PNG), sonst Video-Poster _p bzw. das Link-Bild.
$path = $size === 'ls' ? "$dir/links/{$base}_s.jpg" : "$dir/{$base}_s.jpg";
if (!is_file($path) && is_file(substr($path, 0, -4) . '.png')) $path = substr($path, 0, -4) . '.png';
if (!is_file($path)) {
if ($size === 'ls') {
$srcPath = is_file("$dir/links/{$base}_m.jpg") ? "$dir/links/{$base}_m.jpg" : "$dir/links/$base.jpg";
} else {
$srcPath = is_file("$dir/{$base}_t.jpg") ? "$dir/{$base}_t.jpg"
: (is_file("$dir/{$base}_t.png") ? "$dir/{$base}_t.png" : "$dir/{$base}_p.jpg");
}
// Ziel-Endung folgt der Quelle (PNG behält den Alphakanal, z. B. Sticker)
$path = substr($path, 0, -4) . (substr($srcPath, -4) === '.png' ? '.png' : '.jpg');
if (!term_media_mini($srcPath, $path)) $path = $srcPath; // Fallback: Quelle ausliefern
}
$mime = substr($path, -4) === '.png' ? 'image/png' : 'image/jpeg';
} elseif ($size === 'a' || $size === 'h') {
// Profilfotos (Verzeichnis „profiles"): a = klein (Avatar), h = groß (Header)
$path = "$dir/profiles/{$base}" . ($size === 'a' ? '_t' : '_m') . '.jpg';
if (!is_file($path)) $path = "$dir/profiles/$base.jpg";
$mime = 'image/jpeg';
} elseif ($size === 'v') {
// Video: Medienzeile liefert die Endung (mp4/webm)
$st = $db->prepare('SELECT ext, mime FROM ' . term_tbl('post_media') . " WHERE file_base = ? AND view_id = ? AND media_type = 'video'");
$st->execute([$base, $wid]);
$m = $st->fetch();
if (!$m || !preg_match('/^(mp4|webm)$/', (string)$m['ext'])) { http_response_code(404); exit; }
$path = "$dir/$base.{$m['ext']}";
$mime = (string)$m['mime'];
} else {
$suffix = ['t' => '_t', 'm' => '_m', 'o' => '', 'p' => '_p'][$size];
$path = "$dir/$base$suffix.jpg";
$mime = 'image/jpeg';
// Transparente Bilder (Handy-Sticker) liegen als PNG mit Alphakanal vor
if (!is_file($path) && is_file("$dir/$base$suffix.png")) {
$path = "$dir/$base$suffix.png";
$mime = 'image/png';
}
}
if (!is_file($path)) { http_response_code(404); exit; }
$fsize = filesize($path);
header('Content-Type: ' . $mime);
header('X-Content-Type-Options: nosniff');
header('Content-Security-Policy: default-src \'none\'');
header('Cache-Control: private, max-age=31536000, immutable'); // Token-Dateinamen sind unveränderlich
header('Accept-Ranges: bytes');
// HTTP-Range (nur ein Bereich; für Video-Scrubbing auf iOS zwingend)
$start = 0; $end = $fsize - 1;
if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d*)-(\d*)/', (string)$_SERVER['HTTP_RANGE'], $m)) {
if ($m[1] !== '') $start = (int)$m[1];
if ($m[2] !== '') $end = min((int)$m[2], $fsize - 1);
if ($m[1] === '' && $m[2] !== '') { $start = max(0, $fsize - (int)$m[2]); $end = $fsize - 1; }
if ($start > $end || $start >= $fsize) {
http_response_code(416);
header("Content-Range: bytes */$fsize");
exit;
}
http_response_code(206);
header("Content-Range: bytes $start-$end/$fsize");
}
header('Content-Length: ' . ($end - $start + 1));
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'HEAD') exit;
$fh = fopen($path, 'rb');
if (!$fh) { http_response_code(500); exit; }
fseek($fh, $start);
$left = $end - $start + 1;
while ($left > 0 && !feof($fh)) {
$chunk = fread($fh, min(262144, $left));
if ($chunk === false) break;
echo $chunk;
$left -= strlen($chunk);
flush();
}
fclose($fh);