| Server IP : 188.114.96.3 / Your IP : 216.73.216.143 Web Server : Apache System : Linux sv058.home.net.pl 6.8.2 husak_dirty_pages_fix #1 SMP PREEMPT_DYNAMIC Tue Apr 2 22:06:51 CEST 2024 x86_64 User : ( 3194769) PHP Version : 8.5.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /polana/admin/ |
Upload File : |
<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/includes/functions.php';
require_login();
$contexts = [
'common' => 'Wspólna pula',
'home' => 'Strona główna',
'menu' => 'Menu',
'splash' => 'Globalna zaślepka',
'home_feature' => 'Sekcja „Aktualna karta”',
'menu_splash' => 'Zaślepka tylko menu',
];
$settingKeys = [
'visual_rotation_mode',
'visual_motion_enabled',
'visual_parallax_enabled',
'visual_motion_speed',
'visual_season_override',
'visual_spring_start',
'visual_summer_start',
'visual_autumn_start',
'visual_winter_start',
];
function visual_admin_dir(string $context): string
{
return dirname(__DIR__) . '/assets/img/seasonal/' . $context;
}
function visual_admin_safe_filename(string $name): string
{
$name = pathinfo($name, PATHINFO_FILENAME);
$name = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $name) ?: $name;
$name = strtolower($name);
$name = preg_replace('/[^a-z0-9_-]+/', '-', $name) ?? 'plik';
$name = trim($name, '-_');
return $name !== '' ? $name : 'plik';
}
function visual_admin_allowed_mimes(): array
{
return [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/avif' => 'avif',
'image/gif' => 'gif',
'video/mp4' => 'mp4',
'video/webm' => 'webm',
];
}
function visual_admin_files(string $context): array
{
$dir = visual_admin_dir($context);
if (!is_dir($dir)) {
return [];
}
$files = [];
foreach (seasonal_visual_supported_extensions() as $ext) {
foreach (glob($dir . '/*.' . $ext) ?: [] as $path) {
if (is_file($path)) {
$files[] = $path;
}
}
}
sort($files, SORT_NATURAL | SORT_FLAG_CASE);
return $files;
}
function visual_static_assets(): array
{
return [
'logo' => [
'label' => 'Logo główne',
'path' => dirname(__DIR__) . '/assets/img/logo.png',
'url' => asset_v('img/logo.png'),
'accept' => ['image/png' => 'png', 'image/webp' => 'webp'],
'note' => 'Logo używane w zwykłej stronie / nagłówku.',
],
'logo_splash' => [
'label' => 'Logo na zaślepce / hero',
'path' => dirname(__DIR__) . '/assets/img/logo-splash.png',
'url' => asset_v('img/logo-splash.png'),
'accept' => ['image/png' => 'png', 'image/webp' => 'webp'],
'note' => 'Logo używane na pełnoekranowej zaślepce i w dużych sekcjach.',
],
'favicon' => [
'label' => 'Favicon',
'path' => dirname(__DIR__) . '/assets/img/favicon.png',
'url' => asset_v('img/favicon.png'),
'accept' => ['image/png' => 'png'],
'note' => 'Mała ikona widoczna w karcie przeglądarki.',
],
'pwa_192' => [
'label' => 'Ikona aplikacji 192×192',
'path' => dirname(__DIR__) . '/assets/img/pwa/icon-192.png',
'url' => asset_v('img/pwa/icon-192.png'),
'accept' => ['image/png' => 'png'],
'note' => 'Ikona używana po instalacji strony jako aplikacji.',
],
'pwa_512' => [
'label' => 'Ikona aplikacji 512×512',
'path' => dirname(__DIR__) . '/assets/img/pwa/icon-512.png',
'url' => asset_v('img/pwa/icon-512.png'),
'accept' => ['image/png' => 'png'],
'note' => 'Duża ikona aplikacji PWA.',
],
];
}
function visual_backup_static_asset(string $path, string $key): void
{
if (!is_file($path)) {
return;
}
$backupDir = dirname(__DIR__) . '/storage/static-asset-backups';
if (!is_dir($backupDir)) {
@mkdir($backupDir, 0700, true);
}
if (!is_dir($backupDir)) {
return;
}
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$backup = $backupDir . '/' . $key . '-' . date('Ymd-His') . '.' . $ext;
@copy($path, $backup);
@chmod($backup, 0600);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = (string)($_POST['action'] ?? 'settings');
try {
if ($action === 'settings') {
$seasonDates = seasonal_interactive_validate_start_dates([
'spring' => (string)($_POST['visual_spring_start'] ?? ''),
'summer' => (string)($_POST['visual_summer_start'] ?? ''),
'autumn' => (string)($_POST['visual_autumn_start'] ?? ''),
'winter' => (string)($_POST['visual_winter_start'] ?? ''),
]);
$_POST['visual_spring_start'] = $seasonDates['spring'];
$_POST['visual_summer_start'] = $seasonDates['summer'];
$_POST['visual_autumn_start'] = $seasonDates['autumn'];
$_POST['visual_winter_start'] = $seasonDates['winter'];
$stmt = db()->prepare(
'INSERT INTO settings (setting_key, setting_value)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)'
);
foreach ($settingKeys as $key) {
$stmt->execute([$key, trim((string)($_POST[$key] ?? ''))]);
}
admin_log('visuals_update', 'Zmieniono rotację, animację i daty pór roku');
flash('success', 'Ustawienia grafik zostały zapisane.');
redirect('/admin/visuals.php');
}
if ($action === 'upload') {
$context = (string)($_POST['context'] ?? '');
if (!isset($contexts[$context])) {
throw new RuntimeException('Nieprawidłowa sekcja grafik.');
}
$dir = visual_admin_dir($context);
if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
throw new RuntimeException('Nie udało się utworzyć katalogu grafik.');
}
$uploads = $_FILES['visuals'] ?? null;
if (!$uploads || !isset($uploads['name']) || !is_array($uploads['name'])) {
throw new RuntimeException('Wybierz co najmniej jeden plik.');
}
$allowed = visual_admin_allowed_mimes();
$finfo = new finfo(FILEINFO_MIME_TYPE);
$saved = 0;
foreach ($uploads['name'] as $i => $originalName) {
$error = (int)($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE);
if ($error === UPLOAD_ERR_NO_FILE) {
continue;
}
if ($error !== UPLOAD_ERR_OK) {
throw new RuntimeException('Błąd wysyłania pliku: ' . $originalName);
}
$tmp = (string)($uploads['tmp_name'][$i] ?? '');
$size = (int)($uploads['size'][$i] ?? 0);
if (!is_uploaded_file($tmp)) {
throw new RuntimeException('Nieprawidłowy upload: ' . $originalName);
}
if ($size > 80 * 1024 * 1024) {
throw new RuntimeException('Plik jest za duży: ' . $originalName . ' (maks. 80 MB).');
}
$mime = (string)$finfo->file($tmp);
if (!isset($allowed[$mime])) {
throw new RuntimeException('Nieobsługiwany format: ' . $originalName . ' (' . $mime . ')');
}
$ext = $allowed[$mime];
$base = visual_admin_safe_filename((string)$originalName);
$filename = $base . '-' . date('Ymd-His') . '-' . bin2hex(random_bytes(3)) . '.' . $ext;
$target = $dir . '/' . $filename;
if (!move_uploaded_file($tmp, $target)) {
throw new RuntimeException('Nie udało się zapisać: ' . $originalName);
}
@chmod($target, 0644);
$saved++;
}
if ($saved < 1) {
throw new RuntimeException('Nie zapisano żadnego pliku.');
}
admin_log('visuals_upload', 'Dodano grafiki: ' . $saved . ' do ' . $context);
flash('success', 'Dodano plików: ' . $saved . '.');
redirect('/admin/visuals.php#biblioteka');
}
if ($action === 'replace_static') {
$assetKey = (string)($_POST['asset_key'] ?? '');
$assets = visual_static_assets();
if (!isset($assets[$assetKey])) {
throw new RuntimeException('Nieprawidłowy element strony.');
}
$upload = $_FILES['static_file'] ?? null;
if (!$upload || (int)($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
throw new RuntimeException('Wybierz plik do podmiany.');
}
$tmp = (string)($upload['tmp_name'] ?? '');
$size = (int)($upload['size'] ?? 0);
if (!is_uploaded_file($tmp)) {
throw new RuntimeException('Nieprawidłowy upload.');
}
if ($size > 10 * 1024 * 1024) {
throw new RuntimeException('Plik jest za duży. Maksymalnie 10 MB.');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = (string)$finfo->file($tmp);
$asset = $assets[$assetKey];
if (!isset($asset['accept'][$mime])) {
throw new RuntimeException('Nieobsługiwany format dla tego elementu: ' . $mime);
}
$target = $asset['path'];
$targetDir = dirname($target);
if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
throw new RuntimeException('Nie udało się utworzyć katalogu docelowego.');
}
visual_backup_static_asset($target, $assetKey);
// Zachowujemy nazwę pliku używaną przez stronę, niezależnie od nazwy uploadu.
if (!move_uploaded_file($tmp, $target)) {
throw new RuntimeException('Nie udało się podmienić pliku.');
}
@chmod($target, 0644);
admin_log('visuals_static_replace', 'Podmieniono element: ' . $assetKey);
flash('success', 'Element został podmieniony. Stara wersja została zarchiwizowana.');
redirect('/admin/visuals.php#aktualne');
}
if ($action === 'delete') {
$context = (string)($_POST['context'] ?? '');
$filename = basename((string)($_POST['filename'] ?? ''));
if (!isset($contexts[$context]) || $filename === '') {
throw new RuntimeException('Nieprawidłowy plik.');
}
$path = visual_admin_dir($context) . '/' . $filename;
$realDir = realpath(visual_admin_dir($context));
$realFile = realpath($path);
if (!$realDir || !$realFile || !str_starts_with($realFile, $realDir . DIRECTORY_SEPARATOR)) {
throw new RuntimeException('Plik nie istnieje albo nie należy do biblioteki.');
}
if (!unlink($realFile)) {
throw new RuntimeException('Nie udało się usunąć pliku.');
}
admin_log('visuals_delete', 'Usunięto grafikę: ' . $context . '/' . $filename);
flash('success', 'Grafika została usunięta.');
redirect('/admin/visuals.php#biblioteka');
}
} catch (Throwable $e) {
flash('error', $e->getMessage());
redirect('/admin/visuals.php');
}
}
$mode = setting('visual_rotation_mode', 'pot');
$motionEnabled = setting('visual_motion_enabled', '1');
$parallaxEnabled = setting('visual_parallax_enabled', '1');
$motionSpeed = (int)setting('visual_motion_speed', '16');
$seasonOverride = setting('visual_season_override', 'auto');
$seasonDates = seasonal_interactive_start_dates();
$seasonRanges = seasonal_interactive_season_ranges($seasonDates);
$counts = seasonal_visual_counts();
render_admin_header('Grafiki sezonowe');
?>
<div class="admin-title-row">
<div>
<h1>Grafiki / ruchome tło</h1>
<p>Tu wrzucasz zdjęcia i krótkie ruchome materiały. FTP nie jest już potrzebne.</p>
</div>
</div>
<form class="form-card form-grid" method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="settings">
<div class="full admin-section-title">
<h2>Jak mają się zmieniać</h2>
</div>
<label class="full">
Tryb grafiki
<select name="visual_rotation_mode">
<option value="pot" <?= $mode === 'pot' ? 'selected' : '' ?>>Garnek / fallback</option>
<option value="seasonal" <?= $mode === 'seasonal' ? 'selected' : '' ?>>Interaktywny sezon – 4 pory roku</option>
<option value="random" <?= $mode === 'random' ? 'selected' : '' ?>>Losowo przy odświeżeniu</option>
<option value="month" <?= $mode === 'month' ? 'selected' : '' ?>>Stały wybór na miesiąc</option>
<option value="quarter" <?= $mode === 'quarter' ? 'selected' : '' ?>>Stały wybór na kwartał</option>
</select>
</label>
<label>
Podgląd pory roku
<select name="visual_season_override">
<option value="auto" <?= $seasonOverride === 'auto' ? 'selected' : '' ?>>Automatycznie wg daty</option>
<option value="spring" <?= $seasonOverride === 'spring' ? 'selected' : '' ?>>Wiosna</option>
<option value="summer" <?= $seasonOverride === 'summer' ? 'selected' : '' ?>>Lato</option>
<option value="autumn" <?= $seasonOverride === 'autumn' ? 'selected' : '' ?>>Jesień</option>
<option value="winter" <?= $seasonOverride === 'winter' ? 'selected' : '' ?>>Zima</option>
</select>
</label>
<div class="full admin-section-title">
<h2>Daty pór roku</h2>
<p>
Domyślnie wpisane są astronomiczne początki pór roku dla Krakowa w 2026. Możesz je później zmienić.
Format: <strong>MM-DD</strong>, np. 03-01.
</p>
</div>
<label>
Start wiosny
<input name="visual_spring_start" value="<?= e($seasonDates['spring']) ?>" placeholder="03-20" pattern="\d{2}-\d{2}">
<small><?= e($seasonRanges['spring']) ?></small>
</label>
<label>
Start lata
<input name="visual_summer_start" value="<?= e($seasonDates['summer']) ?>" placeholder="06-21" pattern="\d{2}-\d{2}">
<small><?= e($seasonRanges['summer']) ?></small>
</label>
<label>
Start jesieni
<input name="visual_autumn_start" value="<?= e($seasonDates['autumn']) ?>" placeholder="09-23" pattern="\d{2}-\d{2}">
<small><?= e($seasonRanges['autumn']) ?></small>
</label>
<label>
Start zimy
<input name="visual_winter_start" value="<?= e($seasonDates['winter']) ?>" placeholder="12-21" pattern="\d{2}-\d{2}">
<small><?= e($seasonRanges['winter']) ?></small>
</label>
<label>
Ruch zdjęć
<select name="visual_motion_enabled">
<option value="1" <?= $motionEnabled === '1' ? 'selected' : '' ?>>Włączony</option>
<option value="0" <?= $motionEnabled === '0' ? 'selected' : '' ?>>Wyłączony</option>
</select>
</label>
<label>
Parallax myszką
<select name="visual_parallax_enabled">
<option value="1" <?= $parallaxEnabled === '1' ? 'selected' : '' ?>>Włączony</option>
<option value="0" <?= $parallaxEnabled === '0' ? 'selected' : '' ?>>Wyłączony</option>
</select>
</label>
<label>
Szybkość zdjęć
<select name="visual_motion_speed">
<option value="12" <?= $motionSpeed === 12 ? 'selected' : '' ?>>12 s – wyraźniej</option>
<option value="16" <?= $motionSpeed === 16 ? 'selected' : '' ?>>16 s – domyślnie</option>
<option value="20" <?= $motionSpeed === 20 ? 'selected' : '' ?>>20 s – spokojnie</option>
<option value="26" <?= $motionSpeed === 26 ? 'selected' : '' ?>>26 s – bardzo subtelnie</option>
</select>
</label>
<div class="full">
<button class="btn btn-primary" type="submit">Zapisz ustawienia</button>
</div>
</form>
<section class="form-card" id="aktualne">
<div class="admin-section-title">
<h2>Aktualne elementy strony</h2>
<p>
Tu są stałe elementy używane przez stronę. Logo i ikony możesz podmienić bez FTP.
</p>
</div>
<div class="visual-static-grid">
<?php foreach (visual_static_assets() as $assetKey => $asset): ?>
<article class="visual-static-card">
<div class="visual-static-preview">
<?php if (is_file($asset['path'])): ?>
<img src="<?= e($asset['url']) ?>" alt="<?= e($asset['label']) ?>">
<?php else: ?>
<div class="visual-static-missing">Brak pliku</div>
<?php endif; ?>
</div>
<div class="visual-static-copy">
<h3><?= e($asset['label']) ?></h3>
<p><?= e($asset['note']) ?></p>
<code><?= e(str_replace(dirname(__DIR__), '', $asset['path'])) ?></code>
</div>
<form method="post" enctype="multipart/form-data">
<?= csrf_field() ?>
<input type="hidden" name="action" value="replace_static">
<input type="hidden" name="asset_key" value="<?= e($assetKey) ?>">
<input type="file" name="static_file" accept="image/png,image/webp" required>
<button class="btn btn-small btn-light" type="submit">Podmień</button>
</form>
</article>
<?php endforeach; ?>
<article class="visual-static-card">
<div class="visual-static-preview visual-pot-preview">
<?php render_pot_visual('pot-visual-admin-preview'); ?>
</div>
<div class="visual-static-copy">
<h3>Zimowy garnek</h3>
<p>Aktualny interaktywny motyw zimowy strony.</p>
<code>/includes/pot_visual.php</code>
</div>
</article>
<article class="visual-static-card">
<div class="visual-static-preview visual-menu-preview">
<iframe src="/menu-karta.svg?v=211" title="Aktualna karta menu"></iframe>
</div>
<div class="visual-static-copy">
<h3>Aktualna karta menu</h3>
<p>
Karta również nie jest zwykłym obrazkiem. Jest generowana automatycznie
z danych menu w bazie.
</p>
<code>/menu-karta.svg</code>
</div>
<a class="btn btn-small btn-light" href="/admin/menus.php">Edytuj menu</a>
</article>
</div>
</section>
<section class="form-card" id="pory-roku">
<div class="admin-section-title">
<h2>Grafiki sezonowe</h2>
<p>Te cztery motywy są już częścią strony i przełączają się automatycznie według ustawionych dat.</p>
</div>
<div class="season-four-grid">
<?php foreach (['spring', 'summer', 'autumn', 'winter'] as $seasonKey): ?>
<article class="season-four-card <?= seasonal_interactive_season() === $seasonKey ? 'is-current' : '' ?>">
<div class="season-four-preview">
<?php render_seasonal_illustration('admin', 'season-four-visual', $seasonKey); ?>
</div>
<div class="season-four-meta">
<div>
<strong><?= e(seasonal_interactive_label($seasonKey)) ?></strong>
<?php if (seasonal_interactive_season() === $seasonKey): ?>
<span class="season-current-badge">Teraz na WWW</span>
<?php endif; ?>
</div>
<small>od <?= e(str_replace('-', '.', $seasonDates[$seasonKey])) ?></small>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<form class="form-card form-grid" method="post" enctype="multipart/form-data">
<?= csrf_field() ?>
<input type="hidden" name="action" value="upload">
<div class="full admin-section-title">
<h2>Dodaj zdjęcia / ruchome materiały</h2>
<p>
Możesz zaznaczyć wiele plików naraz. Obsługiwane:
JPG, PNG, WEBP, AVIF, GIF oraz MP4/WEBM.
GIF i WEBP mogą być animowane. MP4/WEBM odtwarzają się bez dźwięku w pętli.
</p>
</div>
<label>
Gdzie ma się pokazywać
<select name="context" required>
<?php foreach ($contexts as $key => $label): ?>
<option value="<?= e($key) ?>"><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</label>
<label class="full">
Pliki
<input
type="file"
name="visuals[]"
accept=".jpg,.jpeg,.png,.webp,.avif,.gif,.mp4,.webm,image/*,video/mp4,video/webm"
multiple
required
>
</label>
<div class="full">
<button class="btn btn-green" type="submit">Wgraj do strony</button>
</div>
</form>
<section class="form-card" id="biblioteka">
<div class="admin-section-title">
<h2>Dodatkowe zdjęcia i filmy</h2>
<p>
To opcjonalna biblioteka własnych materiałów. Cztery grafiki sezonowe są pokazane wyżej i nie liczą się tutaj jako pliki.
</p>
</div>
<?php foreach ($contexts as $context => $label): ?>
<?php $files = visual_admin_files($context); ?>
<div class="visual-library-section">
<div class="visual-library-head">
<h3><?= e($label) ?></h3>
<span><?= count($files) ?> plików</span>
</div>
<?php if (!$files): ?>
<p class="muted">Brak dodatkowych materiałów — to jest OK.</p>
<?php else: ?>
<div class="visual-library-grid">
<?php foreach ($files as $path): ?>
<?php
$filename = basename($path);
$url = seasonal_visual_public_url($path);
$isVideo = seasonal_visual_is_video($path);
?>
<article class="visual-library-card">
<div class="visual-library-preview">
<?php if ($isVideo): ?>
<video src="<?= e($url) ?>" muted loop autoplay playsinline preload="metadata"></video>
<?php else: ?>
<img src="<?= e($url) ?>" alt="" loading="lazy">
<?php endif; ?>
</div>
<div class="visual-library-meta">
<strong title="<?= e($filename) ?>"><?= e($filename) ?></strong>
<small><?= e(strtoupper(pathinfo($filename, PATHINFO_EXTENSION))) ?></small>
</div>
<form method="post" onsubmit="return confirm('Usunąć ten plik?');">
<?= csrf_field() ?>
<input type="hidden" name="action" value="delete">
<input type="hidden" name="context" value="<?= e($context) ?>">
<input type="hidden" name="filename" value="<?= e($filename) ?>">
<button class="btn btn-small btn-danger" type="submit">Usuń</button>
</form>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</section>
<?php render_admin_footer(); ?>