- 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.
683 lines
19 KiB
PHP
683 lines
19 KiB
PHP
<?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;
|
|
}
|