0, 'path' => '/', 'secure' => isSecureConnection(), 'httponly' => true, 'samesite' => 'Lax', ]); $availableCookies = []; foreach (SESSION_COOKIE_CANDIDATES as $cookieName) { if (isset($_COOKIE[$cookieName]) && $_COOKIE[$cookieName] !== '') { $availableCookies[] = $cookieName; } } if ($availableCookies === []) { session_start(); return; } foreach ($availableCookies as $cookieName) { if (session_name() !== $cookieName) { session_name($cookieName); } session_id((string)$_COOKIE[$cookieName]); session_start(); if (isCurrentSessionAuthenticated()) { return; } session_write_close(); } // No authenticated session found: reopen the first available session. $fallbackCookie = $availableCookies[0]; if (session_name() !== $fallbackCookie) { session_name($fallbackCookie); } session_id((string)$_COOKIE[$fallbackCookie]); session_start(); } function requireAuthenticatedUser(): void { $loginId = (int)($_SESSION['login_id'] ?? 0); $adminId = (int)( $_SESSION['id'] ?? $_SESSION['main_admin_user_id'] ?? $_SESSION['admin_user_id'] ?? 0 ); $sidCmsCookie = (string)($_COOKIE['sidcms'] ?? ''); $hasAdminSid = ( session_name() === 'sidcms' && $sidCmsCookie !== '' && ( hash_equals($sidCmsCookie, session_id()) || hash_equals($sidCmsCookie, (string)($_SESSION['sidcms'] ?? '')) ) ); $hasAdminAuthFlags = isAdminSessionFlagSet(); if ($loginId <= 0 && !($hasAdminSid && ($adminId > 0 || $hasAdminAuthFlags))) { header('Location: ' . LOGIN_REDIRECT, true, 302); exit; } // Release session lock early so parallel media requests do not block. session_write_close(); } function maybeOutputSessionDebug(): void { if (!ENABLE_TEMP_SESSION_DEBUG) { return; } $debugFlag = strtolower(trim((string)($_GET['__debug_session'] ?? '0'))); if (!in_array($debugFlag, ['1', 'true', 'yes'], true)) { return; } $sessionId = session_id(); $sidCmsCookie = (string)($_COOKIE['sidcms'] ?? ''); $loginId = (int)($_SESSION['login_id'] ?? 0); $adminId = (int)( $_SESSION['id'] ?? $_SESSION['main_admin_user_id'] ?? $_SESSION['admin_user_id'] ?? 0 ); $hasAdminSid = ( session_name() === 'sidcms' && $sidCmsCookie !== '' && ( hash_equals($sidCmsCookie, $sessionId) || hash_equals($sidCmsCookie, (string)($_SESSION['sidcms'] ?? '')) ) ); $hasAdminAuthFlags = isAdminSessionFlagSet(); $payload = [ 'debug' => true, 'session_name' => session_name(), 'session_id' => $sessionId, 'cookie_candidates_present' => array_values(array_filter( SESSION_COOKIE_CANDIDATES, static fn(string $name): bool => !empty($_COOKIE[$name]) )), 'auth_evaluation' => [ 'login_id' => $loginId, 'admin_id' => $adminId, 'has_admin_sid' => $hasAdminSid, 'has_admin_auth_flags' => $hasAdminAuthFlags, 'would_pass_auth' => ($loginId > 0) || ($hasAdminSid && ($adminId > 0 || $hasAdminAuthFlags)), ], 'session' => $_SESSION, 'cookies' => $_COOKIE, 'request' => [ 'method' => (string)($_SERVER['REQUEST_METHOD'] ?? ''), 'uri' => (string)($_SERVER['REQUEST_URI'] ?? ''), 'remote_addr' => (string)($_SERVER['REMOTE_ADDR'] ?? ''), ], ]; while (ob_get_level() > 0) { ob_end_clean(); } header('Content-Type: application/json; charset=UTF-8'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); echo json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } function isCurrentSessionAuthenticated(): bool { $loginId = (int)($_SESSION['login_id'] ?? 0); if ($loginId > 0) { return true; } if (session_name() !== 'sidcms') { return false; } $adminId = (int)( $_SESSION['id'] ?? $_SESSION['main_admin_user_id'] ?? $_SESSION['admin_user_id'] ?? 0 ); if ($adminId <= 0 && !isAdminSessionFlagSet()) { return false; } $sidCmsCookie = (string)($_COOKIE['sidcms'] ?? ''); if ($sidCmsCookie === '') { return false; } return hash_equals($sidCmsCookie, session_id()) || hash_equals($sidCmsCookie, (string)($_SESSION['sidcms'] ?? '')); } function isAdminSessionFlagSet(): bool { if (!empty($_SESSION['IsAuthorized'])) { return true; } if ((int)($_SESSION['session_login'] ?? 0) > 0) { return true; } if ((int)($_SESSION['admin_login'] ?? 0) > 0) { return true; } return false; } function serveRequestedFile(): void { $requestMethod = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')); if (!in_array($requestMethod, ['GET', 'HEAD'], true)) { header('Allow: GET, HEAD', true, 405); exit; } $relativePath = requestedRelativePath(); if ($relativePath === '') { http_response_code(400); echo 'Bad Request'; exit; } $baseDir = realpath(USERDATA_BASE_DIR); if ($baseDir === false || !is_dir($baseDir)) { http_response_code(500); echo 'Storage Not Configured'; exit; } $resolvedPath = realpath($baseDir . DIRECTORY_SEPARATOR . $relativePath); if ($resolvedPath === false || !is_file($resolvedPath) || !is_readable($resolvedPath)) { http_response_code(404); echo 'Not Found'; exit; } if (!isInsideBasePath($resolvedPath, $baseDir)) { http_response_code(403); echo 'Forbidden'; exit; } deliverFile($resolvedPath, $relativePath, $requestMethod === 'HEAD'); } function requestedRelativePath(): string { // PHP already decodes $_GET values; do not decode again. $rawPath = (string)($_GET['file'] ?? ''); $rawPath = trim(str_replace('\\', '/', $rawPath)); $rawPath = ltrim($rawPath, '/'); $rawPath = preg_replace('#/+#', '/', $rawPath) ?? ''; if ($rawPath === '' || str_contains($rawPath, "\0")) { return ''; } $segments = explode('/', $rawPath); foreach ($segments as $segment) { if ($segment === '' || $segment === '.' || $segment === '..') { return ''; } } return $rawPath; } function isInsideBasePath(string $resolvedPath, string $baseDir): bool { $base = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; return strncmp($resolvedPath, $base, strlen($base)) === 0; } function deliverFile(string $absolutePath, string $relativePath, bool $headOnly): void { $fileSize = filesize($absolutePath); if ($fileSize === false || $fileSize < 0) { http_response_code(500); echo 'Unable to read file metadata'; exit; } $lastModifiedTs = filemtime($absolutePath); if ($lastModifiedTs === false) { $lastModifiedTs = time(); } $mimeType = detectSafeMimeType($absolutePath, $relativePath); $isInlineSvg = ($mimeType === 'image/svg+xml'); $isMedia = isMediaMimeType($mimeType); $etag = buildEtag($absolutePath, $fileSize, $lastModifiedTs); $ifNoneMatch = trim((string)($_SERVER['HTTP_IF_NONE_MATCH'] ?? '')); if ($ifNoneMatch !== '' && requestEtagMatches($ifNoneMatch, $etag)) { sendNotModified($etag, $lastModifiedTs, $isMedia, $isInlineSvg); } $ifModifiedSince = trim((string)($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '')); if ($ifNoneMatch === '' && $ifModifiedSince !== '' && requestNotModifiedSince($ifModifiedSince, $lastModifiedTs)) { sendNotModified($etag, $lastModifiedTs, $isMedia, $isInlineSvg); } $useXSendfile = shouldUseXSendfile(); $rangeRequest = ['status' => 'none']; if ($isMedia && !$useXSendfile) { $rangeRequest = parseRangeHeader((string)($_SERVER['HTTP_RANGE'] ?? ''), $fileSize); if ($rangeRequest['status'] === 'invalid') { sendRangeNotSatisfiable($fileSize, $etag, $lastModifiedTs, $isMedia); } } sendSharedSecurityHeaders($isInlineSvg); sendCacheHeaders($isMedia); header('Content-Type: ' . $mimeType); header('ETag: "' . $etag . '"'); header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModifiedTs) . ' GMT'); header('Content-Disposition: inline; filename="' . sanitizeFilenameForHeader(basename($absolutePath)) . '"'); if ($isMedia) { header('Accept-Ranges: bytes'); } if ($useXSendfile) { header('X-Sendfile: ' . $absolutePath); exit; } if ($rangeRequest['status'] !== 'none' || ALLOW_PHP_STREAM_FALLBACK || !USE_X_SENDFILE) { streamFileWithPhp($absolutePath, $fileSize, $headOnly, $rangeRequest); } // X-Sendfile requested, but not available and fallback disabled. http_response_code(503); echo 'File delivery backend unavailable'; exit; } function sendSharedSecurityHeaders(bool $isInlineSvg = false): void { if ($isInlineSvg) { // Inline SVG only for image requests with a strict CSP. header("Content-Security-Policy: default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'none'; object-src 'none'; base-uri 'none'; sandbox"); header('X-Content-Type-Options: nosniff'); return; } header("Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox"); header('X-Content-Type-Options: nosniff'); } function sendCacheHeaders(bool $isMedia): void { if ($isMedia) { header('Cache-Control: private, max-age=' . (string)MEDIA_CACHE_MAX_AGE . ', must-revalidate'); return; } header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: 0'); } function sendNotModified(string $etag, int $lastModifiedTs, bool $isMedia, bool $isInlineSvg = false): void { http_response_code(304); sendSharedSecurityHeaders($isInlineSvg); sendCacheHeaders($isMedia); header('ETag: "' . $etag . '"'); header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModifiedTs) . ' GMT'); exit; } function sendRangeNotSatisfiable(int $fileSize, string $etag, int $lastModifiedTs, bool $isMedia): void { http_response_code(416); sendSharedSecurityHeaders(); sendCacheHeaders($isMedia); header('ETag: "' . $etag . '"'); header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModifiedTs) . ' GMT'); header('Content-Range: bytes */' . (string)$fileSize); exit; } function shouldUseXSendfile(): bool { if (!USE_X_SENDFILE) { return false; } if (!function_exists('apache_get_modules')) { return false; } $modules = apache_get_modules(); return in_array('mod_xsendfile', $modules, true) || in_array('mod_xsendfile.c', $modules, true); } function streamFileWithPhp(string $absolutePath, int $fileSize, bool $headOnly, array $rangeRequest): void { $start = 0; $end = $fileSize > 0 ? $fileSize - 1 : 0; $statusCode = 200; if ($rangeRequest['status'] === 'range') { $start = $rangeRequest['start']; $end = $rangeRequest['end']; $statusCode = 206; header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize); } $length = ($fileSize > 0) ? ($end - $start + 1) : 0; http_response_code($statusCode); header('Content-Length: ' . (string)$length); if ($headOnly || $length <= 0) { exit; } $handle = fopen($absolutePath, 'rb'); if ($handle === false) { http_response_code(500); echo 'Unable to open file stream'; exit; } while (ob_get_level() > 0) { ob_end_clean(); } if ($start > 0) { fseek($handle, $start); } $bytesRemaining = $length; while ($bytesRemaining > 0 && !feof($handle)) { $chunkSize = (int)min(STREAM_CHUNK_SIZE, $bytesRemaining); $buffer = fread($handle, $chunkSize); if ($buffer === false || $buffer === '') { break; } echo $buffer; flush(); $bytesRemaining -= strlen($buffer); } fclose($handle); exit; } function parseRangeHeader(string $rangeHeader, int $fileSize): array { $rangeHeader = trim($rangeHeader); if ($rangeHeader === '') { return ['status' => 'none']; } if ($fileSize <= 0 || !str_starts_with($rangeHeader, 'bytes=')) { return ['status' => 'invalid']; } $rangeValue = trim(substr($rangeHeader, 6)); if ($rangeValue === '' || str_contains($rangeValue, ',')) { return ['status' => 'invalid']; } if (!preg_match('/^(\d*)-(\d*)$/', $rangeValue, $matches)) { return ['status' => 'invalid']; } $startRaw = $matches[1]; $endRaw = $matches[2]; if ($startRaw === '' && $endRaw === '') { return ['status' => 'invalid']; } if ($startRaw === '') { $suffixLength = (int)$endRaw; if ($suffixLength <= 0) { return ['status' => 'invalid']; } $suffixLength = min($suffixLength, $fileSize); $start = $fileSize - $suffixLength; $end = $fileSize - 1; return ['status' => 'range', 'start' => $start, 'end' => $end]; } $start = (int)$startRaw; if ($start >= $fileSize) { return ['status' => 'invalid']; } if ($endRaw === '') { $end = $fileSize - 1; } else { $end = (int)$endRaw; if ($end < $start) { return ['status' => 'invalid']; } $end = min($end, $fileSize - 1); } return ['status' => 'range', 'start' => $start, 'end' => $end]; } function requestEtagMatches(string $ifNoneMatchHeader, string $etag): bool { $ifNoneMatchHeader = trim($ifNoneMatchHeader); if ($ifNoneMatchHeader === '*') { return true; } $parts = explode(',', $ifNoneMatchHeader); foreach ($parts as $part) { $candidate = trim($part); if ($candidate === '') { continue; } if (str_starts_with($candidate, 'W/')) { $candidate = substr($candidate, 2); } $candidate = trim($candidate, " \t\n\r\0\x0B\""); if ($candidate === $etag) { return true; } } return false; } function requestNotModifiedSince(string $ifModifiedSinceHeader, int $lastModifiedTs): bool { $requestTs = strtotime($ifModifiedSinceHeader); if ($requestTs === false) { return false; } return $lastModifiedTs <= $requestTs; } function buildEtag(string $absolutePath, int $fileSize, int $lastModifiedTs): string { return sha1($absolutePath . '|' . $fileSize . '|' . $lastModifiedTs); } function isMediaMimeType(string $mimeType): bool { $normalized = strtolower(trim($mimeType)); if ($normalized === '') { return false; } if (str_starts_with($normalized, 'image/') || str_starts_with($normalized, 'video/') || str_starts_with($normalized, 'audio/')) { return true; } return in_array($normalized, [ 'application/dash+xml', 'application/x-mpegurl', 'application/vnd.apple.mpegurl', 'vnd.apple.mpegurl', ], true); } function detectSafeMimeType(string $absolutePath, string $relativePath): string { $mimeType = 'application/octet-stream'; $finfo = finfo_open(FILEINFO_MIME_TYPE); if ($finfo !== false) { $detected = finfo_file($finfo, $absolutePath); finfo_close($finfo); if (is_string($detected) && $detected !== '') { $mimeType = $detected; } } $dangerousMimeTypes = [ 'text/html', 'application/xhtml+xml', 'image/svg+xml', 'text/xml', 'application/xml', 'text/xsl', 'application/javascript', 'text/javascript', ]; if ($mimeType === 'image/svg+xml' && shouldServeSvgInline($relativePath)) { return $mimeType; } if (in_array($mimeType, $dangerousMimeTypes, true)) { return 'application/octet-stream'; } return $mimeType; } function shouldServeSvgInline(string $relativePath): bool { if (!ENABLE_INLINE_SVG_FOR_IMAGE_REQUESTS) { return false; } $fetchDest = strtolower(trim((string)($_SERVER['HTTP_SEC_FETCH_DEST'] ?? ''))); if ($fetchDest !== '' && $fetchDest !== 'image') { return false; } if ($fetchDest === '') { $accept = strtolower(trim((string)($_SERVER['HTTP_ACCEPT'] ?? ''))); if ($accept === '' || !str_contains($accept, 'image/')) { return false; } } $normalizedPath = strtolower(ltrim(str_replace('\\', '/', $relativePath), '/')); if ($normalizedPath === '') { return false; } if (INLINE_SVG_ALLOWED_PATH_PREFIXES === []) { return true; } foreach (INLINE_SVG_ALLOWED_PATH_PREFIXES as $prefix) { $normalizedPrefix = strtolower(ltrim(str_replace('\\', '/', (string)$prefix), '/')); if ($normalizedPrefix === '') { continue; } $normalizedPrefix = rtrim($normalizedPrefix, '/') . '/'; if (str_starts_with($normalizedPath, $normalizedPrefix)) { return true; } } return false; } function sanitizeFilenameForHeader(string $filename): string { return preg_replace('/[^A-Za-z0-9._-]/', '_', $filename) ?? 'download'; } function isSecureConnection(): bool { $https = strtolower((string)($_SERVER['HTTPS'] ?? '')); if ($https !== '' && $https !== 'off') { return true; } if ((int)($_SERVER['SERVER_PORT'] ?? 80) === 443) { return true; } if (TRUSTED_PROXY_HEADER) { $forwardedProto = strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')); return $forwardedProto === 'https'; } return false; }