player, image previews, and * "Download original" links. * * ACL: enforces folder read permission on the document. */ declare(strict_types=1); require_once dirname(__DIR__, 2) . '/includes/bootstrap.php'; dbnToolsRequireAuth(); try { $tenant = dbnToolsEnsureDashboardTenant(); } catch (DbnToolsHttpException $e) { dbnToolsError($e->getMessage(), $e->status, $e->errorCode); } $clientId = (int)$tenant['client_id']; $userId = (int)($tenant['client_user_id'] ?? 0); $tenantRole = (string)($tenant['role'] ?? 'editor'); $id = (int)($_GET['id'] ?? 0); $versionId = (int)($_GET['version_id'] ?? 0); $download = !empty($_GET['download']); if ($id <= 0) { dbnToolsError('id is required.', 400, 'missing_id'); } $db = dbnToolsDb(); $stmt = $db->prepare('SELECT id, folder_id, storage_path, original_filename, source_type FROM client_documents WHERE id = ? AND client_id = ?'); $stmt->execute([$id, $clientId]); $doc = $stmt->fetch(); if (!$doc) { dbnToolsError('Document not found.', 404, 'not_found'); } $fid = $doc['folder_id'] ? (int)$doc['folder_id'] : 0; if (!dbnDmsUserCanAccessFolder($fid ?: null, 'read', $clientId, $userId, $tenantRole)) { dbnToolsError('Forbidden.', 403, 'forbidden'); } $path = (string)($doc['storage_path'] ?? ''); $filename = (string)($doc['original_filename'] ?? ''); if ($versionId > 0) { $vs = $db->prepare('SELECT storage_path, original_filename FROM client_document_versions WHERE id = ? AND document_id = ? AND client_id = ?'); $vs->execute([$versionId, $id, $clientId]); $ver = $vs->fetch(); if (!$ver) { dbnToolsError('Version not found.', 404, 'version_not_found'); } $path = (string)($ver['storage_path'] ?? ''); $filename = (string)($ver['original_filename'] ?? $filename); } if ($path === '' || !is_file($path) || !is_readable($path)) { dbnToolsError('Original file is not available for this document.', 404, 'file_missing', ['hint' => 'Document predates disk storage, or file was purged.']); } $ext = dbnDmsExtensionFromFilename($filename); if ($ext === '' && $path !== '') { $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); } $contentType = dbnDmsContentTypeForExt($ext); dbnDmsLogAudit($clientId, $userId ?: null, $download ? 'download' : 'preview', ['version_id' => $versionId ?: null, 'ext' => $ext], $id, $fid ?: null); // Suppress any earlier output (defensive). if (ob_get_level() > 0) { @ob_end_clean(); } $size = filesize($path) ?: 0; header('Content-Type: ' . $contentType); header('Content-Length: ' . $size); header('X-Content-Type-Options: nosniff'); header('Cache-Control: private, max-age=300'); $disposition = $download ? 'attachment' : 'inline'; $safeName = $filename !== '' ? $filename : ('document-' . $id . '.' . ($ext ?: 'bin')); $safeName = preg_replace('/[\r\n"]/', '_', $safeName) ?? $safeName; header(sprintf('Content-Disposition: %s; filename="%s"', $disposition, $safeName)); // Range requests (basic) — useful for PDF.js + audio scrubbing. $range = $_SERVER['HTTP_RANGE'] ?? ''; if ($range && preg_match('/bytes=(\d+)-(\d+)?/', $range, $m)) { $start = (int)$m[1]; $end = isset($m[2]) && $m[2] !== '' ? (int)$m[2] : ($size - 1); if ($end >= $size) $end = $size - 1; $length = $end - $start + 1; http_response_code(206); header('Accept-Ranges: bytes'); header("Content-Range: bytes {$start}-{$end}/{$size}"); header('Content-Length: ' . $length); $fh = fopen($path, 'rb'); if ($fh) { fseek($fh, $start); $remaining = $length; while (!feof($fh) && $remaining > 0) { $chunk = fread($fh, min(8192, $remaining)); if ($chunk === false) break; echo $chunk; $remaining -= strlen($chunk); @ob_flush(); @flush(); } fclose($fh); } exit; } header('Accept-Ranges: bytes'); readfile($path); exit;