Refactor contact management functions and remove phpinfo.php
- Updated formatting and structure of functions in edit_contact.inc.php for better readability and consistency. - Added comments to clarify the purpose of certain code sections, especially regarding mandant handling. - Removed the deprecated phpinfo.php file to clean up the codebase.
This commit is contained in:
682
mysyde/frontend/frontend_userdata_controller.php
Normal file
682
mysyde/frontend/frontend_userdata_controller.php
Normal file
@@ -0,0 +1,682 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
const SESSION_COOKIE_CANDIDATES = ['sidintranet', 'sidcms', 'mysyde', 'PHPSESSID', 'sidawo'];
|
||||
const LOGIN_REDIRECT = '/';
|
||||
const USERDATA_BASE_DIR = __DIR__ . '/../../userdata';
|
||||
const USE_X_SENDFILE = true;
|
||||
const ALLOW_PHP_STREAM_FALLBACK = true;
|
||||
const MEDIA_CACHE_MAX_AGE = 3600;
|
||||
const TRUSTED_PROXY_HEADER = true;
|
||||
const STREAM_CHUNK_SIZE = 1048576;
|
||||
const ENABLE_INLINE_SVG_FOR_IMAGE_REQUESTS = true;
|
||||
const ENABLE_TEMP_SESSION_DEBUG = false;
|
||||
// Optional hardening: e.g. ['icons/', 'logos/']; empty array allows all paths.
|
||||
const INLINE_SVG_ALLOWED_PATH_PREFIXES = [];
|
||||
|
||||
// Main flow:
|
||||
// 1. Start session (cookie hardening)
|
||||
// 2. Enforce authentication
|
||||
// 3. Validate + deliver requested file (X-Sendfile preferred, PHP fallback)
|
||||
bootstrapSession();
|
||||
maybeOutputSessionDebug();
|
||||
requireAuthenticatedUser();
|
||||
serveRequestedFile();
|
||||
|
||||
function bootstrapSession(): void
|
||||
{
|
||||
ini_set('session.use_only_cookies', '1');
|
||||
ini_set('session.use_strict_mode', '1');
|
||||
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 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;
|
||||
}
|
||||
Reference in New Issue
Block a user