* Liefert eine Datei aus DOL_DATA_ROOT aus, authentifiziert per JWT. * Whitelist: nur facture/, commande/, propal/, bericht/ * * Query: * relpath — relativer Pfad unter DOL_DATA_ROOT * size=thumb&w= — serverseitig erzeugtes Thumbnail (GD, gecacht, mit ETag). * Das ist der Weg für Kachel-Ansichten. w: 40–800, Default 320. * size=small|mini — altes Verhalten: nutzt ein von Dolibarr vorgefertigtes * thumbs/_small., falls vorhanden. Für Dateien aus * der PWA existiert so eines nicht (kein vignette() beim Upload). * download=1 — als Anhang ausliefern statt inline */ // Dieser Endpoint liefert Binärdaten aus — KEIN JSON Content-Type! // Deshalb nicht _inc.php nutzen, sondern awlauth + Dolibarr manuell laden. if (!defined('NOLOGIN')) define('NOLOGIN', '1'); if (!defined('NOCSRFCHECK')) define('NOCSRFCHECK', '1'); if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1'); if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1'); if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1'); if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1'); $res = 0; if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; $tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; $tmp2 = realpath(__FILE__); $i = strlen($tmp) - 1; $j = strlen($tmp2) - 1; while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; } if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php"; if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php"; if (!$res) die("Include of main fails"); require_once __DIR__.'/../lib/bericht.lib.php'; // Auth über das zentrale SSO-Cookie awl_sso. Bewusst awlauth_verify (OHNE CSRF), // weil dieser reine GET-Read auch per window.location/ geladen wird // (Download-Link, Audio-Element) und dort kein X-Requested-With setzbar ist. if (!dol_include_once('/awlauth/lib/awlauth.lib.php') || !function_exists('awlauth_verify')) { http_response_code(500); header('Content-Type: text/plain'); echo 'SSO-Modul (awlauth) nicht verfügbar'; exit; } $user = awlauth_verify(); if (!$user || !$user->hasRight('bericht', 'read')) { http_response_code(401); header('Content-Type: text/plain'); echo 'Nicht angemeldet'; exit; } $relpath = (string) ($_GET['relpath'] ?? ''); $size = (string) ($_GET['size'] ?? ''); if (empty($relpath)) { http_response_code(400); header('Content-Type: text/plain'); echo 'relpath fehlt'; exit; } // Whitelist if (!preg_match('#^(facture|commande|propal|bericht)/#', $relpath)) { http_response_code(403); header('Content-Type: text/plain'); echo 'Pfad nicht erlaubt: '.$relpath; exit; } $full = bericht_resolve_data_path($relpath); if (!$full || !file_exists($full)) { http_response_code(404); header('Content-Type: text/plain'); echo 'Datei nicht gefunden: '.$relpath; exit; } // Serverseitig erzeugtes Thumbnail (GD, gecacht) — der Weg für die Kachel-Ansicht der PWA. // // Die alte Variante size=small/mini konnte nur ein Thumbnail ausliefern, das Dolibarr selbst // unter thumbs/_small. abgelegt hatte. Fotos aus der PWA kommen aber über // orders.php?action=upload_photo herein, und dort läuft kein vignette() — es gab also nie // eines, und jede Kachel lud das komplette Handyfoto. bericht_attachment_thumb() (seit 1.5.0, // auch vom Editor genutzt) rendert bei Bedarf und legt das Ergebnis unter bericht/thumbs/ ab. if ($size === 'thumb') { $w = (int) ($_GET['w'] ?? 320); if ($w < 40) $w = 40; if ($w > 800) $w = 800; // ETag über mtime: ein ersetztes Original liefert einen neuen Wert, sonst antwortet // der Server mit 304 und überträgt gar nichts. $etag = '"'.sha1($relpath.'|'.@filemtime($full).'|'.$w).'"'; header('ETag: '.$etag); header('Cache-Control: private, max-age=2592000'); if (trim((string) ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '')) === $etag) { http_response_code(304); exit; } $thumb_data = bericht_attachment_thumb($full, $w); if ($thumb_data !== null) { header('Content-Type: image/jpeg'); header('Content-Length: '.strlen($thumb_data)); echo $thumb_data; exit; } // Kein unterstütztes Bild (PDF/Video) oder zu groß für den Speicher: // unten regulär das Original ausliefern. } // Alt-Variante: von Dolibarr vorgefertigtes Thumbnail, falls vorhanden if ($size === 'small' || $size === 'mini') { $dir = dirname($full); $base = pathinfo($full, PATHINFO_FILENAME); $ext = pathinfo($full, PATHINFO_EXTENSION); $thumb = $dir.'/thumbs/'.$base.'_'.$size.'.'.$ext; if (file_exists($thumb)) $full = $thumb; } $mime = function_exists('dol_mimetype') ? dol_mimetype($full) : 'application/octet-stream'; $download = !empty($_GET['download']); $filename = $download ? basename($full) : ''; header('Content-Type: '.$mime); header('Accept-Ranges: bytes'); header('Cache-Control: private, max-age=3600'); if ($download) { header('Content-Disposition: attachment; filename="'.addslashes($filename).'"'); } // Teilbereiche (Range, seit 1.8.0): Ein Video aus der Baustellen-App spielt sonst erst, wenn // es komplett geladen ist, und lässt sich nicht spulen. Nur ein einzelner Bereich — bei // mehreren (selten) geht wie bisher die ganze Datei raus, das ist erlaubt. $groesse = (int) filesize($full); $von = 0; $bis = $groesse - 1; if (!empty($_SERVER['HTTP_RANGE']) && preg_match('/^bytes=(\d*)-(\d*)$/', trim($_SERVER['HTTP_RANGE']), $m) && ($m[1] !== '' || $m[2] !== '')) { if ($m[1] === '') { $von = max(0, $groesse - (int) $m[2]); // „bytes=-500": die letzten 500 Bytes } else { $von = (int) $m[1]; if ($m[2] !== '') $bis = min((int) $m[2], $groesse - 1); } if ($von > $bis || $von >= $groesse) { http_response_code(416); header('Content-Range: bytes */'.$groesse); exit; } http_response_code(206); header('Content-Range: bytes '.$von.'-'.$bis.'/'.$groesse); } $laenge = $bis - $von + 1; header('Content-Length: '.$laenge); if ($von === 0 && $laenge === $groesse) { readfile($full); exit; } $fh = fopen($full, 'rb'); if ($fh === false) exit; fseek($fh, $von); $rest = $laenge; while ($rest > 0 && !feof($fh)) { $stueck = fread($fh, min(1048576, $rest)); if ($stueck === false || $stueck === '') break; echo $stueck; $rest -= strlen($stueck); } fclose($fh); exit;