'image', 'image/jpg' => 'image', 'image/png' => 'image', 'image/gif' => 'image', 'image/webp' => 'image', 'image/avif' => 'image', 'image/svg+xml' => 'image', 'video/mp4' => 'video', 'video/quicktime' => 'video', // .mov 'video/webm' => 'video', 'video/ogg' => 'video', 'video/x-msvideo' => 'video', // .avi 'video/x-matroska'=> 'video', // .mkv ])); // ══════════════════════════════════════════════════════════════════════════════ header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); function upload_ok(array $data): never { echo json_encode(['ok' => true] + $data, JSON_UNESCAPED_SLASHES); exit; } function upload_err(string $msg, int $code = 400): never { http_response_code($code); echo json_encode(['ok' => false, 'error' => $msg]); exit; } // ── Auth ────────────────────────────────────────────────────────────────────── if (!file_exists(UPLOAD_KEY_FILE)) upload_err('API key file not configured.', 503); $supplied = trim( $_SERVER['HTTP_X_API_KEY'] ?? $_SERVER['HTTP_X_Api_Key'] ?? ($_GET['api_key'] ?? '') ); if ($supplied === '') upload_err('Missing API key.', 401); $valid_keys = array_filter( array_map('trim', file(UPLOAD_KEY_FILE)), fn($l) => $l !== '' && !str_starts_with($l, '#') ); $authed = false; foreach ($valid_keys as $k) { if (hash_equals($k, $supplied)) { $authed = true; break; } } if (!$authed) upload_err('Invalid API key.', 403); // ── Request validation ──────────────────────────────────────────────────────── if ($_SERVER['REQUEST_METHOD'] !== 'POST') upload_err('POST required.', 405); if (empty($_FILES['file'])) upload_err('No file received.'); $file = $_FILES['file']; $error = $file['error'] ?? UPLOAD_ERR_NO_FILE; if ($error === UPLOAD_ERR_INI_SIZE || $error === UPLOAD_ERR_FORM_SIZE) upload_err('File exceeds size limit.'); if ($error !== UPLOAD_ERR_OK) upload_err('Upload error code: ' . $error); if ($file['size'] > UPLOAD_MAX_BYTES) upload_err('File too large (max ' . round(UPLOAD_MAX_BYTES / 1048576) . ' MB).'); // ── MIME detection (finfo, not extension trust) ─────────────────────────────── $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']); $allowed = unserialize(UPLOAD_ALLOWED); if (!isset($allowed[$mime])) { upload_err('File type not allowed: ' . $mime . '. Allowed: JPEG, PNG, GIF, WEBP, AVIF, SVG, MP4, MOV, WEBM, OGG, AVI, MKV.'); } $media_type = $allowed[$mime]; // ── Generate unique filename preserving original extension ──────────────────── $orig_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); // Normalise common aliases $ext_map = ['jpg' => 'jpg', 'jpeg' => 'jpg', 'jpe' => 'jpg', 'png' => 'png', 'gif' => 'gif', 'webp' => 'webp', 'avif' => 'avif', 'svg' => 'svg', 'mp4' => 'mp4', 'mov' => 'mov', 'webm' => 'webm', 'ogg' => 'ogg', 'ogv' => 'ogg', 'avi' => 'avi', 'mkv' => 'mkv']; $safe_ext = $ext_map[$orig_ext] ?? $orig_ext; $filename = bin2hex(random_bytes(12)) . '.' . $safe_ext; $dest = UPLOAD_DIR . $filename; // ── Create upload directory if it doesn't exist ─────────────────────────────── if (!is_dir(UPLOAD_DIR)) { if (!mkdir(UPLOAD_DIR, 0755, true)) { upload_err('Cannot create upload directory.', 500); } // Drop an index.html to prevent directory listing file_put_contents(UPLOAD_DIR . 'index.html', ''); } // ── Move file ───────────────────────────────────────────────────────────────── if (!move_uploaded_file($file['tmp_name'], $dest)) { upload_err('Failed to save file.', 500); } // ── Respond ─────────────────────────────────────────────────────────────────── upload_ok([ 'url' => UPLOAD_URL . '/' . $filename, 'filename' => $filename, 'type' => $media_type, // 'image' | 'video' 'mime' => $mime, 'size' => $file['size'], ]);