0,
'path' => '/',
'secure' => $httpsOn, // cookie только по HTTPS
'httponly' => true, // недоступна из JS
'samesite' => 'Strict', // защита от CSRF
]);
session_start();
// ==== Транслитерация кириллицы -> латиница ====
function transliterate(string $text): string
{
$map = [
'а'=>'a','б'=>'b','в'=>'v','г'=>'g','д'=>'d','е'=>'e','ё'=>'e','ж'=>'zh',
'з'=>'z','и'=>'i','й'=>'y','к'=>'k','л'=>'l','м'=>'m','н'=>'n','о'=>'o',
'п'=>'p','р'=>'r','с'=>'s','т'=>'t','у'=>'u','ф'=>'f','х'=>'kh','ц'=>'ts',
'ч'=>'ch','ш'=>'sh','щ'=>'shch','ъ'=>'','ы'=>'y','ь'=>'','э'=>'e','ю'=>'yu','я'=>'ya',
'А'=>'A','Б'=>'B','В'=>'V','Г'=>'G','Д'=>'D','Е'=>'E','Ё'=>'E','Ж'=>'Zh',
'З'=>'Z','И'=>'I','Й'=>'Y','К'=>'K','Л'=>'L','М'=>'M','Н'=>'N','О'=>'O',
'П'=>'P','Р'=>'R','С'=>'S','Т'=>'T','У'=>'U','Ф'=>'F','Х'=>'Kh','Ц'=>'Ts',
'Ч'=>'Ch','Ш'=>'Sh','Щ'=>'Shch','Ъ'=>'','Ы'=>'Y','Ь'=>'','Э'=>'E','Ю'=>'Yu','Я'=>'Ya',
// украинские/белорусские на всякий случай
'і'=>'i','І'=>'I','ї'=>'yi','Ї'=>'Yi','є'=>'ye','Є'=>'Ye','ґ'=>'g','Ґ'=>'G','ў'=>'u','Ў'=>'U',
];
return strtr($text, $map);
}
// ==== Очистка имени файла ====
function sanitizeFilename(string $name): string
{
$name = transliterate($name);
$ext = pathinfo($name, PATHINFO_EXTENSION);
$base = pathinfo($name, PATHINFO_FILENAME);
// пробелы -> подчёркивания, удаляем всё кроме безопасных символов
$base = preg_replace('/\s+/u', '_', $base);
$base = preg_replace('/[^A-Za-z0-9._-]/', '', $base);
$base = trim($base, '.-_');
if ($base === '') {
$base = 'file_' . date('Ymd_His');
}
$ext = preg_replace('/[^A-Za-z0-9]/', '', $ext);
return $ext !== '' ? $base . '.' . strtolower($ext) : $base;
}
// ==== Уникальное имя, чтобы не перезаписать существующее ====
function uniqueFilename(string $dir, string $filename): string
{
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$base = pathinfo($filename, PATHINFO_FILENAME);
$candidate = $filename;
$i = 1;
while (file_exists($dir . DIRECTORY_SEPARATOR . $candidate)) {
$candidate = $ext !== '' ? "{$base}-{$i}.{$ext}" : "{$base}-{$i}";
$i++;
}
return $candidate;
}
// ==== Работа с папками ====
// Очистка одного сегмента пути (имени папки)
function sanitizeSegment(string $seg): string
{
$seg = transliterate($seg);
$seg = preg_replace('/\s+/u', '_', $seg);
$seg = preg_replace('/[^A-Za-z0-9._-]/', '', $seg);
return trim($seg, '.-_');
}
// Приводит относительный путь папки к безопасному виду (без выхода за пределы корня)
function normalizeFolder(string $rel): string
{
$rel = str_replace('\\', '/', $rel);
$parts = [];
foreach (explode('/', $rel) as $seg) {
if ($seg === '' || $seg === '.' || $seg === '..') {
continue;
}
$clean = sanitizeSegment($seg);
if ($clean !== '') {
$parts[] = $clean;
}
}
return implode('/', $parts);
}
// Список всех подпапок относительно корня (с ограничением глубины)
function listFolders(string $base, string $prefix = '', int $depth = 0): array
{
if ($depth > 6 || !is_dir($base)) {
return [];
}
$result = [];
$entries = @scandir($base) ?: [];
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..' || $entry[0] === '.') {
continue;
}
$full = $base . DIRECTORY_SEPARATOR . $entry;
if (is_dir($full)) {
$rel = $prefix === '' ? $entry : $prefix . '/' . $entry;
$result[] = $rel;
$result = array_merge($result, listFolders($full, $rel, $depth + 1));
}
}
return $result;
}
// Собирает публичный URL с учётом относительного пути папки
function buildPublicUrl(string $folderRel, string $filename): string
{
$url = rtrim(PUBLIC_BASE_URL, '/') . '/';
if ($folderRel !== '') {
$url .= implode('/', array_map('rawurlencode', explode('/', $folderRel))) . '/';
}
return $url . rawurlencode($filename);
}
function isAuthenticated(): bool
{
return !empty($_SESSION['authenticated']);
}
// ==== Защита от перебора пароля (по IP, хранится во временной папке вне веб-корня) ====
function throttlePath(): string
{
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'uploader_login_' . md5($ip) . '.json';
}
function throttleState(): array
{
$f = throttlePath();
if (is_file($f)) {
$d = json_decode((string)@file_get_contents($f), true);
if (is_array($d)) {
return ['attempts' => (int)($d['attempts'] ?? 0), 'lock_until' => (int)($d['lock_until'] ?? 0)];
}
}
return ['attempts' => 0, 'lock_until' => 0];
}
function throttleSave(int $attempts, int $lockUntil): void
{
@file_put_contents(throttlePath(), json_encode(['attempts' => $attempts, 'lock_until' => $lockUntil]), LOCK_EX);
}
function jsonResponse(array $data, int $code = 200): void
{
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
// ==== Маршрутизация ====
$action = $_GET['action'] ?? ($_POST['action'] ?? '');
// Выход
if ($action === 'logout') {
$_SESSION = [];
session_destroy();
header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
exit;
}
// Логин
if ($action === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$now = time();
$state = throttleState();
// блокировка активна — сразу отказ
if ($state['lock_until'] > $now) {
$wait = $state['lock_until'] - $now;
jsonResponse(['ok' => false, 'error' => "Слишком много попыток. Повторите через {$wait} сек."], 429);
}
$pass = $_POST['password'] ?? '';
if (password_verify((string)$pass, PASSWORD_HASH)) {
throttleSave(0, 0); // сброс счётчика
session_regenerate_id(true); // защита от фиксации сессии
$_SESSION['authenticated'] = true;
jsonResponse(['ok' => true]);
}
// неудача — наращиваем счётчик и при превышении включаем блокировку
$attempts = $state['attempts'] + 1;
$lockUntil = 0;
if ($attempts >= LOGIN_MAX_ATTEMPTS) {
$lockUntil = $now + min(LOGIN_LOCK_MAX, 30 * (2 ** ($attempts - LOGIN_MAX_ATTEMPTS)));
}
throttleSave($attempts, $lockUntil);
usleep(400000); // небольшая задержка против быстрого перебора
if ($lockUntil > $now) {
$wait = $lockUntil - $now;
jsonResponse(['ok' => false, 'error' => "Слишком много попыток. Повторите через {$wait} сек."], 429);
}
jsonResponse(['ok' => false, 'error' => 'Неверный пароль'], 401);
}
// Список папок
if ($action === 'folders') {
if (!isAuthenticated()) {
jsonResponse(['ok' => false, 'error' => 'Не авторизован'], 401);
}
$folders = listFolders(UPLOAD_DIR);
sort($folders, SORT_NATURAL | SORT_FLAG_CASE);
jsonResponse(['ok' => true, 'folders' => $folders]);
}
// Создание папки
if ($action === 'mkdir' && $_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isAuthenticated()) {
jsonResponse(['ok' => false, 'error' => 'Не авторизован'], 401);
}
$parent = normalizeFolder($_POST['parent'] ?? '');
$name = sanitizeSegment($_POST['name'] ?? '');
if ($name === '') {
jsonResponse(['ok' => false, 'error' => 'Некорректное имя папки'], 400);
}
$rel = $parent === '' ? $name : $parent . '/' . $name;
$abs = UPLOAD_DIR . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $rel);
if (is_dir($abs)) {
jsonResponse(['ok' => true, 'folder' => $rel, 'existed' => true]);
}
if (!mkdir($abs, 0755, true) && !is_dir($abs)) {
jsonResponse(['ok' => false, 'error' => 'Не удалось создать папку'], 500);
}
jsonResponse(['ok' => true, 'folder' => $rel]);
}
// Загрузка файла
if ($action === 'upload' && $_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isAuthenticated()) {
jsonResponse(['ok' => false, 'error' => 'Не авторизован'], 401);
}
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
$errCode = $_FILES['file']['error'] ?? -1;
jsonResponse(['ok' => false, 'error' => 'Ошибка загрузки (код ' . $errCode . ')'], 400);
}
$file = $_FILES['file'];
if (MAX_FILE_SIZE > 0 && $file['size'] > MAX_FILE_SIZE) {
jsonResponse(['ok' => false, 'error' => 'Файл слишком большой'], 400);
}
// целевая папка (по умолчанию — корень)
$folderRel = normalizeFolder($_POST['folder'] ?? '');
$destDir = UPLOAD_DIR . ($folderRel !== '' ? DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $folderRel) : '');
if ($folderRel !== '' && !is_dir($destDir)) {
if (!mkdir($destDir, 0755, true) && !is_dir($destDir)) {
jsonResponse(['ok' => false, 'error' => 'Не удалось создать папку'], 500);
}
}
$clean = sanitizeFilename($file['name']);
$replace = !empty($_POST['replace']) && $_POST['replace'] !== '0' && $_POST['replace'] !== 'false';
// при включённой замене пишем поверх существующего, иначе подбираем уникальное имя
$finalName = $replace ? $clean : uniqueFilename($destDir, $clean);
$target = $destDir . DIRECTORY_SEPARATOR . $finalName;
if (!move_uploaded_file($file['tmp_name'], $target)) {
jsonResponse(['ok' => false, 'error' => 'Не удалось сохранить файл'], 500);
}
jsonResponse([
'ok' => true,
'name' => $finalName,
'original' => $file['name'],
'folder' => $folderRel,
'url' => buildPublicUrl($folderRel, $finalName),
'size' => $file['size'],
]);
}
?>
Загрузка файлов Вход
Введите пароль для доступа к загрузчику
Загрузка файлов
Перетащите файлы — ссылки появятся ниже
⬆️
Перетащите файлы сюда
или нажмите, чтобы выбрать