<?php
declare(strict_types=1);

/**
 * /functional/files/file.php
 * Віддає файл по share-token:
 * - public: доступний будь-кому з посиланням
 * - private: тільки власнику (логін)
 * Параметри:
 * - t=token (обов'язково)
 * - dl=1 (форс download)
 *
 * SEO: будь-які файли по токену => NOINDEX (X-Robots-Tag)
 *
 * Безпека:
 * - шлях лише всередині /upload
 * - небезпечні MIME (html/svg тощо) -> тільки attachment (download)
 */

/*
  ВАЖЛИВО:
  /functional/files/* => до кореня сайту треба піднятися на 2 рівні:
  files -> functional -> ROOT
*/
require_once dirname(__DIR__, 2) . '/includes/start.php';

/** @var PDO|null $pdo */
/** @var array|null $me */
$pdo = (isset($pdo) && $pdo instanceof PDO) ? $pdo : db();
$me  = $me ?? current_user();

$token = trim((string)($_GET['t'] ?? ''));
$dl    = (int)($_GET['dl'] ?? 0);

if ($token === '' || !preg_match('~^[a-f0-9]{64}$~', $token)) {
    http_response_code(400);
    exit('Bad Request');
}

/* Бери тільки потрібні поля */
$st = $pdo->prepare("
    SELECT
      user_id,
      is_public,
      rel_path,
      mime,
      original_name
    FROM myfiles
    WHERE share_token = ?
    LIMIT 1
");
$st->execute([$token]);
$f = $st->fetch(PDO::FETCH_ASSOC);

if (!$f) {
    http_response_code(404);
    exit('Not Found');
}

$isPublic = (int)($f['is_public'] ?? 0) === 1;
$ownerId  = (int)($f['user_id'] ?? 0);
$viewerId = (int)($me['id'] ?? 0);

/* Private: краще 404, щоб не палити існування токена */
if (!$isPublic) {
    if (!$me || $viewerId <= 0 || $viewerId !== $ownerId) {
        http_response_code(404);
        exit('Not Found');
    }
}

/* SEO / robots + anti-cache */
if (!headers_sent()) {
    header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet', true);
    header('X-Content-Type-Options: nosniff', true);
    header('Referrer-Policy: same-origin', true);
    header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0', true);
    header('Pragma: no-cache', true);
    header('Expires: 0', true);
}

/* Визначаємо корінь проєкту та /upload */
$root = realpath(dirname(__DIR__, 2));
if ($root === false) $root = dirname(__DIR__, 2);

$uploadsRoot = realpath($root . '/upload');
if ($uploadsRoot === false) {
    http_response_code(500);
    exit('Server Error');
}

/* Нормалізація rel_path */
$rel = (string)($f['rel_path'] ?? '');
$rel = str_replace(["\\", "\0"], ['/', ''], $rel);
$rel = preg_replace('~[\r\n]~', '', $rel);
$rel = preg_replace('~\.\.+~', '', $rel);
$rel = ltrim($rel, '/');

/* Додатково: тільки всередині upload */
if ($rel === '' || !str_starts_with($rel, 'upload/')) {
    http_response_code(404);
    exit('File missing');
}

$abs = realpath($root . '/' . $rel);
if ($abs === false || !is_file($abs)) {
    http_response_code(404);
    exit('File missing');
}

/* Перевірка що $abs реально всередині /upload (з захистом від prefix-bypass) */
$uploadsRootNorm = rtrim($uploadsRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$absNorm         = rtrim($abs, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;

if (strpos($absNorm, $uploadsRootNorm) !== 0) {
    http_response_code(404);
    exit('File missing');
}

/* MIME: валідний формат, без ін’єкцій */
$mime = trim((string)($f['mime'] ?? ''));
if ($mime !== '') {
    $mime = trim(explode(';', $mime, 2)[0]); // прибрати charset/параметри
}
if ($mime === '' || !preg_match('~^[a-z0-9][a-z0-9\.\+\-]*/[a-z0-9][a-z0-9\.\+\-]*$~i', $mime)) {
    $mime = 'application/octet-stream';
}

/* Якщо є finfo — можна підстрахувати */
if (function_exists('finfo_open')) {
    try {
        $fi = finfo_open(FILEINFO_MIME_TYPE);
        if ($fi) {
            $det = finfo_file($fi, $abs);
            finfo_close($fi);
            if (is_string($det) && $det !== '' && preg_match('~^[a-z0-9][a-z0-9\.\+\-]*/[a-z0-9][a-z0-9\.\+\-]*$~i', $det)) {
                $mime = $det;
            }
        }
    } catch (Throwable $e) {
        // ignore
    }
}

/* filename safe + RFC5987 */
$orig = (string)($f['original_name'] ?? 'file');
$orig = preg_replace('~[\x00-\x1F\x7F]~u', '', $orig) ?? '';
$orig = str_replace(['"', ';', "\r", "\n"], '', $orig);
$orig = trim($orig);
if ($orig === '') $orig = 'file';

/* ASCII fallback for filename="", UTF-8 for filename*= */
$origAscii = preg_replace('~[^\x20-\x7E]~', '_', $orig) ?? 'file';
$origAscii = trim($origAscii);
if ($origAscii === '') $origAscii = 'file';

if (strlen($orig) > 180) $orig = substr($orig, 0, 180);
if (strlen($origAscii) > 180) $origAscii = substr($origAscii, 0, 180);

$encoded = rawurlencode($orig);

/* Безпечний inline: забороняємо html/svg та інші “виконувані” */
$inlineAllowed = false;
if (str_starts_with($mime, 'image/')) {
    $inlineAllowed = ($mime !== 'image/svg+xml');
} elseif (str_starts_with($mime, 'audio/')) {
    $inlineAllowed = true;
} elseif (str_starts_with($mime, 'video/')) {
    $inlineAllowed = true;
} elseif (in_array($mime, ['application/pdf', 'text/plain'], true)) {
    $inlineAllowed = true;
}

$forceAttachment = ($dl === 1) || !$inlineAllowed;
$disposition = $forceAttachment ? 'attachment' : 'inline';

$filesize = filesize($abs);
if ($filesize === false) $filesize = 0;

/* Важливо для range: вимкнути компресію якщо раптом вмикалась */
@ini_set('zlib.output_compression', '0');
@set_time_limit(0);

/* Відпустити lock сесії, щоб інші запити не чекали завершення скачування */
if (session_status() === PHP_SESSION_ACTIVE) {
    @session_write_close();
}

/* Заголовки */
header('Content-Type: ' . $mime);
header(
    'Content-Disposition: ' . $disposition .
    '; filename="' . $origAscii . '"' .
    "; filename*=UTF-8''" . $encoded
);
header('Accept-Ranges: bytes');
header('Content-Length: ' . $filesize);

/* HEAD: тільки заголовки */
$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
if ($method === 'HEAD') {
    exit;
}

/* Range support (один діапазон) */
$range = (string)($_SERVER['HTTP_RANGE'] ?? '');
if ($range && preg_match('~bytes=(\d+)-(\d*)~', $range, $m)) {
    $start = (int)$m[1];
    $end   = ($m[2] !== '') ? (int)$m[2] : ($filesize - 1);

    if ($filesize > 0 && $end >= $filesize) $end = $filesize - 1;
    if ($start < 0) $start = 0;

    if ($filesize === 0 || $start > $end) {
        http_response_code(416);
        header('Content-Range: bytes */' . $filesize);
        exit;
    }

    $len = $end - $start + 1;

    http_response_code(206);
    header('Content-Length: ' . $len);
    header('Content-Range: bytes ' . $start . '-' . $end . '/' . $filesize);

    $fp = fopen($abs, 'rb');
    if (!$fp) {
        http_response_code(500);
        exit('Read error');
    }

    fseek($fp, $start);

    $chunk = 1048576; // 1MB
    $sent  = 0;

    while (!feof($fp) && $sent < $len) {
        $need = min($chunk, $len - $sent);
        $buf  = fread($fp, $need);
        if ($buf === false || $buf === '') break;

        echo $buf;
        $sent += strlen($buf);
        @flush();
    }

    fclose($fp);
    exit;
}

/* Full stream */
$fp = fopen($abs, 'rb');
if (!$fp) {
    http_response_code(500);
    exit('Read error');
}

while (!feof($fp)) {
    $buf = fread($fp, 1048576);
    if ($buf === false || $buf === '') break;
    echo $buf;
    @flush();
}

fclose($fp);
exit;