gallery-for-aav/admin.php
2026-02-19 17:46:46 +03:00

332 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
const MAX_UPLOAD_BYTES = 3 * 1024 * 1024;
$rootDir = __DIR__;
$photosDir = $rootDir . '/photos';
$thumbsDir = $rootDir . '/thumbs';
$dataDir = $rootDir . '/data';
$sortFile = $dataDir . '/sort.json';
@mkdir($photosDir, 0775, true);
@mkdir($thumbsDir, 0775, true);
@mkdir($dataDir, 0775, true);
$configPath = __DIR__ . '/deploy-config.php';
if (!is_file($configPath)) {
http_response_code(500);
echo 'deploy-config.php not found';
exit;
}
$config = require $configPath;
$tokenExpected = (string)($config['token'] ?? '');
$tokenIncoming = (string)($_REQUEST['token'] ?? '');
if ($tokenExpected === '' || !hash_equals($tokenExpected, $tokenIncoming)) {
http_response_code(403);
echo 'Forbidden';
exit;
}
$sortData = loadSortData($sortFile);
$sortData = reconcileSortData($photosDir, $sortData);
saveSortData($sortFile, $sortData);
$message = '';
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = (string)($_POST['action'] ?? '');
if ($action === 'create_category') {
$name = sanitizeCategoryName((string)($_POST['category_name'] ?? ''));
if ($name === '') {
$errors[] = 'Некорректное имя папки.';
} else {
$dir = $photosDir . '/' . $name;
if (!is_dir($dir) && !mkdir($dir, 0775, true)) {
$errors[] = 'Не удалось создать папку.';
} else {
$message = 'Папка создана: ' . $name;
$sortData['categories'][$name] = nextSortIndex($sortData['categories']);
}
}
}
if ($action === 'category_update') {
$current = sanitizeCategoryName((string)($_POST['category_current'] ?? ''));
$newName = sanitizeCategoryName((string)($_POST['category_new_name'] ?? ''));
$sortIndex = (int)($_POST['category_sort'] ?? 1000);
if ($current === '' || !is_dir($photosDir . '/' . $current)) {
$errors[] = 'Категория не найдена.';
} else {
if ($newName !== '' && $newName !== $current) {
$oldDir = $photosDir . '/' . $current;
$newDir = $photosDir . '/' . $newName;
$oldThumb = $thumbsDir . '/' . $current;
$newThumb = $thumbsDir . '/' . $newName;
if (is_dir($newDir)) {
$errors[] = 'Категория с таким именем уже существует.';
} else {
rename($oldDir, $newDir);
if (is_dir($oldThumb)) {
@rename($oldThumb, $newThumb);
}
if (isset($sortData['categories'][$current])) {
$sortData['categories'][$newName] = $sortData['categories'][$current];
unset($sortData['categories'][$current]);
}
if (isset($sortData['photos'][$current])) {
$sortData['photos'][$newName] = $sortData['photos'][$current];
unset($sortData['photos'][$current]);
}
$current = $newName;
$message = 'Категория переименована.';
}
}
$sortData['categories'][$current] = $sortIndex;
$message = $message ?: 'Категория обновлена.';
}
}
if ($action === 'category_delete') {
$category = sanitizeCategoryName((string)($_POST['category_current'] ?? ''));
if ($category === '' || !is_dir($photosDir . '/' . $category)) {
$errors[] = 'Категория не найдена.';
} else {
rrmdir($photosDir . '/' . $category);
rrmdir($thumbsDir . '/' . $category);
unset($sortData['categories'][$category], $sortData['photos'][$category]);
$message = 'Категория удалена: ' . $category;
}
}
if ($action === 'upload') {
$category = sanitizeCategoryName((string)($_POST['category'] ?? ''));
if ($category === '' || !is_dir($photosDir . '/' . $category)) {
$errors[] = 'Выберите существующую категорию.';
} elseif (!isset($_FILES['photos'])) {
$errors[] = 'Файлы не переданы.';
} else {
$result = handleUploads($_FILES['photos'], $photosDir . '/' . $category, $sortData, $category);
$errors = array_merge($errors, $result['errors']);
if ($result['ok'] > 0) {
$message = 'Загружено: ' . $result['ok'];
}
}
}
if ($action === 'photo_update') {
$category = sanitizeCategoryName((string)($_POST['category'] ?? ''));
$currentFile = basename((string)($_POST['photo_current'] ?? ''));
$newBase = sanitizeFileBase((string)($_POST['photo_new_name'] ?? ''));
$sortIndex = (int)($_POST['photo_sort'] ?? 1000);
$src = $photosDir . '/' . $category . '/' . $currentFile;
if ($category === '' || $currentFile === '' || !is_file($src)) {
$errors[] = 'Фото не найдено.';
} else {
$finalName = $currentFile;
if ($newBase !== '') {
$ext = strtolower(pathinfo($currentFile, PATHINFO_EXTENSION));
$candidate = uniqueFileNameForRename($photosDir . '/' . $category, $newBase, $ext, $currentFile);
if ($candidate !== $currentFile) {
$dst = $photosDir . '/' . $category . '/' . $candidate;
if (@rename($src, $dst)) {
$oldThumb = $thumbsDir . '/' . $category . '/' . pathinfo($currentFile, PATHINFO_FILENAME) . '.jpg';
$newThumb = $thumbsDir . '/' . $category . '/' . pathinfo($candidate, PATHINFO_FILENAME) . '.jpg';
if (is_file($oldThumb)) {
@rename($oldThumb, $newThumb);
}
if (isset($sortData['photos'][$category][$currentFile])) {
$sortData['photos'][$category][$candidate] = $sortData['photos'][$category][$currentFile];
unset($sortData['photos'][$category][$currentFile]);
}
$finalName = $candidate;
}
}
}
$sortData['photos'][$category][$finalName] = $sortIndex;
$message = 'Фото обновлено.';
}
}
if ($action === 'photo_delete') {
$category = sanitizeCategoryName((string)($_POST['category'] ?? ''));
$file = basename((string)($_POST['photo_current'] ?? ''));
$src = $photosDir . '/' . $category . '/' . $file;
if ($category === '' || $file === '' || !is_file($src)) {
$errors[] = 'Фото не найдено.';
} else {
@unlink($src);
$thumb = $thumbsDir . '/' . $category . '/' . pathinfo($file, PATHINFO_FILENAME) . '.jpg';
if (is_file($thumb)) {
@unlink($thumb);
}
unset($sortData['photos'][$category][$file]);
$message = 'Фото удалено.';
}
}
saveSortData($sortFile, $sortData);
}
$categories = listCategories($photosDir, $sortData);
$selectedCategory = sanitizeCategoryName((string)($_GET['edit_category'] ?? ($_POST['category'] ?? '')));
$photos = $selectedCategory !== '' ? listPhotos($photosDir, $thumbsDir, $selectedCategory, $sortData) : [];
?><!doctype html>
<html lang="ru"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Админка галереи</title>
<link rel="icon" type="image/svg+xml" href="<?= h(assetUrl('favicon.svg')) ?>">
<link rel="stylesheet" href="<?= h(assetUrl('style.css')) ?>">
<style>
.admin-wrap{max-width:1150px;margin:0 auto;padding:24px}.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:14px;margin-bottom:14px}
.grid{display:grid;gap:12px;grid-template-columns:1fr 1fr}.full{grid-column:1/-1}.in{width:100%;padding:8px;border:1px solid #d1d5db;border-radius:8px}
.btn{border:0;background:#1f6feb;color:#fff;padding:8px 12px;border-radius:8px;cursor:pointer}.btn-danger{background:#b42318}.muted{color:#667085;font-size:13px}
.table{width:100%;border-collapse:collapse}.table td,.table th{padding:8px;border-bottom:1px solid #eee;vertical-align:top}.ok{background:#ecfdf3;padding:8px;border-radius:8px;margin-bottom:8px}.err{background:#fef2f2;padding:8px;border-radius:8px;margin-bottom:8px}
@media (max-width:900px){.grid{grid-template-columns:1fr}}
</style>
</head><body><div class="admin-wrap">
<h1>Админка галереи</h1>
<p><a href="./">← В галерею</a></p>
<?php if ($message !== ''): ?><div class="ok"><?= h($message) ?></div><?php endif; ?>
<?php foreach ($errors as $e): ?><div class="err"><?= h($e) ?></div><?php endforeach; ?>
<div class="grid">
<section class="card">
<h3>Создать папку</h3>
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>">
<input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="action" value="create_category">
<input class="in" name="category_name" placeholder="например: Тест" required>
<p style="margin-top:8px"><button class="btn" type="submit">Создать</button></p>
</form>
</section>
<section class="card full">
<h3>Категории (редактирование / сортировка / удаление)</h3>
<table class="table"><tr><th>Категория</th><th>Порядок</th><th>Новое имя</th><th>Действия</th></tr>
<?php foreach ($categories as $c): ?>
<tr>
<td><a href="?token=<?= urlencode($tokenIncoming) ?>&edit_category=<?= urlencode($c) ?>"><?= h($c) ?></a></td>
<td>
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>">
<input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="action" value="category_update">
<input type="hidden" name="category_current" value="<?= h($c) ?>">
<input class="in" name="category_sort" type="number" value="<?= (int)($sortData['categories'][$c] ?? 1000) ?>">
</td>
<td><input class="in" name="category_new_name" value="<?= h($c) ?>"></td>
<td style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn" type="submit">Сохранить</button>
</form>
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>" onsubmit="return confirm('Удалить категорию и все фото?')">
<input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="action" value="category_delete"><input type="hidden" name="category_current" value="<?= h($c) ?>">
<button class="btn btn-danger" type="submit">Удалить</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</table>
</section>
<section class="card full">
<h3>Фото в категории: <?= h($selectedCategory ?: '—') ?></h3>
<?php if ($selectedCategory !== ''): ?>
<form method="post" enctype="multipart/form-data" action="?token=<?= urlencode($tokenIncoming) ?>&edit_category=<?= urlencode($selectedCategory) ?>" style="margin:10px 0 14px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="action" value="upload">
<input type="hidden" name="category" value="<?= h($selectedCategory) ?>">
<input type="file" name="photos[]" accept="image/jpeg,image/png,image/webp,image/gif" multiple required>
<button class="btn" type="submit">Загрузить в «<?= h($selectedCategory) ?>»</button>
</form>
<p class="muted" style="margin-top:-6px">Только JPG/PNG/WEBP/GIF, максимум 3 МБ на файл.</p>
<?php endif; ?>
<?php if ($selectedCategory === ''): ?>
<p class="muted">Сначала выбери категорию в блоке выше (клик по её названию).</p>
<?php elseif ($photos === []): ?>
<p class="muted">В категории пока нет фото.</p>
<?php else: ?>
<table class="table"><tr><th>Превью</th><th>Фото</th><th>Порядок</th><th>Новое имя (без расширения)</th><th>Действия</th></tr>
<?php foreach ($photos as $p): ?>
<tr>
<td><?php if ($p['thumb'] !== ''): ?><img src="<?= h($p['thumb']) ?>" alt="" style="width:78px;height:52px;object-fit:cover;border-radius:6px;border:1px solid #e5e7eb"><?php endif; ?></td>
<td><?= h($p['file']) ?></td>
<td>
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>&edit_category=<?= urlencode($selectedCategory) ?>">
<input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="action" value="photo_update">
<input type="hidden" name="category" value="<?= h($selectedCategory) ?>"><input type="hidden" name="photo_current" value="<?= h($p['file']) ?>">
<input class="in" type="number" name="photo_sort" value="<?= (int)$p['sort'] ?>">
</td>
<td><input class="in" name="photo_new_name" value="<?= h(pathinfo($p['file'], PATHINFO_FILENAME)) ?>"></td>
<td style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn" type="submit">Сохранить</button>
</form>
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>&edit_category=<?= urlencode($selectedCategory) ?>" onsubmit="return confirm('Удалить фото?')">
<input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="action" value="photo_delete">
<input type="hidden" name="category" value="<?= h($selectedCategory) ?>"><input type="hidden" name="photo_current" value="<?= h($p['file']) ?>">
<button class="btn btn-danger" type="submit">Удалить</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
</section>
</div></div></body></html>
<?php
function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function assetUrl(string $relativePath): string { $file=__DIR__ . '/' . ltrim($relativePath, '/'); $v=is_file($file)?(string)filemtime($file):(string)time(); return $relativePath . '?v=' . rawurlencode($v); }
function sanitizeCategoryName(string $name): string { $name=trim($name); $name=preg_replace('/[^\p{L}\p{N}\s._-]+/u','',$name)??''; return trim($name,". \t\n\r\0\x0B"); }
function sanitizeFileBase(string $name): string { $name=trim($name); $name=preg_replace('/[^\p{L}\p{N}._-]+/u','_',$name)??''; return trim($name,'._-'); }
function loadSortData(string $file): array { if(!is_file($file)) return ['categories'=>[],'photos'=>[]]; $d=json_decode((string)file_get_contents($file),true); return is_array($d)?['categories'=>(array)($d['categories']??[]),'photos'=>(array)($d['photos']??[])]:['categories'=>[],'photos'=>[]]; }
function saveSortData(string $file, array $data): void { file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); }
function nextSortIndex(array $map): int { return $map===[]?10:((int)max(array_map('intval',$map))+10); }
function listCategories(string $photosDir, array $sortData): array { $out=[]; foreach((@scandir($photosDir)?:[]) as $x){ if($x==='.'||$x==='..')continue; if(is_dir($photosDir.'/'.$x))$out[]=$x; } usort($out, fn($a,$b)=>((int)($sortData['categories'][$a]??1000)<=> (int)($sortData['categories'][$b]??1000)) ?: strnatcasecmp($a,$b)); return $out; }
function listPhotos(string $photosDir, string $thumbsDir, string $category, array $sortData): array { $out=[]; $dir=$photosDir.'/'.$category; foreach((@scandir($dir)?:[]) as $f){ if($f==='.'||$f==='..')continue; $p=$dir.'/'.$f; if(!is_file($p)||!isImageExt($f)) continue; $thumbAbs=$thumbsDir.'/'.$category.'/'.pathinfo($f, PATHINFO_FILENAME).'.jpg'; $thumb=is_file($thumbAbs)?('thumbs/'.rawurlencode($category).'/'.rawurlencode(pathinfo($f, PATHINFO_FILENAME).'.jpg')):''; $out[]=['file'=>$f,'sort'=>(int)($sortData['photos'][$category][$f]??1000),'thumb'=>$thumb]; } usort($out, fn($a,$b)=>($a['sort']<=>$b['sort']) ?: strnatcasecmp($a['file'],$b['file'])); return $out; }
function reconcileSortData(string $photosDir, array $sortData): array {
$clean=['categories'=>[],'photos'=>[]];
$cats=[];
foreach((@scandir($photosDir)?:[]) as $c){
if($c==='.'||$c==='..') continue;
if(!is_dir($photosDir.'/'.$c)) continue;
$cats[]=$c;
}
foreach($cats as $c){
$clean['categories'][$c]=(int)($sortData['categories'][$c] ?? 1000);
$clean['photos'][$c]=[];
foreach((@scandir($photosDir.'/'.$c)?:[]) as $f){
if($f==='.'||$f==='..') continue;
if(!is_file($photosDir.'/'.$c.'/'.$f) || !isImageExt($f)) continue;
$clean['photos'][$c][$f]=(int)($sortData['photos'][$c][$f] ?? 1000);
}
}
return $clean;
}
function isImageExt(string $file): bool { return in_array(strtolower(pathinfo($file, PATHINFO_EXTENSION)), ['jpg','jpeg','png','webp','gif'], true); }
function rrmdir(string $dir): void { if(!is_dir($dir)) return; $it=scandir($dir)?:[]; foreach($it as $x){ if($x==='.'||$x==='..')continue; $p=$dir.'/'.$x; if(is_dir($p)) rrmdir($p); else @unlink($p);} @rmdir($dir); }
function uniqueFileNameForRename(string $dir,string $base,string $ext,string $current): string{ $n=0; do{ $cand=$n===0?"{$base}.{$ext}":"{$base}_{$n}.{$ext}"; if($cand===$current||!file_exists($dir.'/'.$cand)) return $cand; $n++; }while(true); }
function handleUploads(array $files, string $targetDir, array &$sortData, string $category): array {
$allowedMime=['image/jpeg','image/png','image/webp','image/gif']; $allowedExt=['jpg','jpeg','png','webp','gif']; $ok=0; $errors=[];
$names=$files['name']??[]; $tmp=$files['tmp_name']??[]; $sizes=$files['size']??[]; $errs=$files['error']??[];
if(!is_array($names)){ $names=[$names]; $tmp=[$tmp]; $sizes=[$sizes]; $errs=[$errs]; }
$finfo=finfo_open(FILEINFO_MIME_TYPE);
foreach($names as $i=>$orig){ if((int)($errs[$i]??UPLOAD_ERR_NO_FILE)!==UPLOAD_ERR_OK){$errors[]="{$orig}: ошибка загрузки";continue;}
$size=(int)($sizes[$i]??0); if($size<1||$size>MAX_UPLOAD_BYTES){$errors[]="{$orig}: >3MB";continue;}
$tmpFile=(string)($tmp[$i]??''); if($tmpFile===''||!is_uploaded_file($tmpFile)){ $errors[]="{$orig}: источник"; continue;}
$mime=$finfo?(string)finfo_file($finfo,$tmpFile):''; if(!in_array($mime,$allowedMime,true)){ $errors[]="{$orig}: тип {$mime}"; continue;}
$ext=strtolower(pathinfo((string)$orig, PATHINFO_EXTENSION)); if(!in_array($ext,$allowedExt,true)){ $errors[]="{$orig}: расширение"; continue;}
$base=sanitizeFileBase(pathinfo((string)$orig, PATHINFO_FILENAME)); if($base==='')$base='photo'; $name=uniqueFileNameForRename($targetDir,$base,$ext,'');
if(!move_uploaded_file($tmpFile,$targetDir.'/'.$name)){ $errors[]="{$orig}: не сохранить"; continue; }
$sortData['photos'][$category][$name]=nextSortIndex((array)($sortData['photos'][$category]??[])); $ok++;
}
if($finfo)finfo_close($finfo);
return ['ok'=>$ok,'errors'=>$errors];
}