<?php
/**
* CMS: LaiCMS (Edition 2026)
* Файл: admin/add_post.php
* Описание: Умный редактор публикаций с полной русской локализацией.
*/
require_once '../system/db.php';
require_once '../system/functions.php';
/**
* Исправление ошибки: Определение функции логирования
*/
if (!function_exists('write_log')) {
function write_log($message, $type = 'info') {
global $mysqli;
// Если в БД есть таблица logs — пишем туда, если нет — в системный лог PHP
$log_msg = "[" . date('Y-m-d H:i:s') . "] [$type] $message" . PHP_EOL;
error_log($log_msg);
}
}
// Защита доступа
if (!function_exists('isAdmin') || !isAdmin()) {
write_log("Попытка доступа к редактору без прав администратора", "security");
header("Location: /users/login.php");
exit;
}
/**
* Ядро обработки контента (ContentCore)
*/
class ContentCore {
public static function createSlug(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','х'=>'h','ц'=>'c','ч'=>'ch','ш'=>'sh','щ'=>'sch','ъ'=>'','ы'=>'y','ь'=>'','э'=>'e','ю'=>'yu','я'=>'ya'];
$slug = strtr(mb_strtolower($text), $map);
$slug = preg_replace('/[^a-z0-9-]+/', '-', $slug);
return trim($slug, '-') . '-' . bin2hex(random_bytes(2));
}
public static function validate(string $title, string $content): array {
$errors = [];
if (mb_strlen($title) < 10) $errors[] = "Заголовок слишком короткий (минимум 10 символов).";
if (mb_strlen($content) < 50) $errors[] = "Текст статьи слишком мал для публикации.";
return $errors;
}
}
// Обработчик загрузки изображений из редактора
if (!empty($_FILES['file'])) {
$img_dir = '../uploads/blog/';
if (!is_dir($img_dir)) mkdir($img_dir, 0755, true);
$f = $_FILES['file'];
$allowed = ['image/jpeg', 'image/png', 'image/webp'];
if (in_array($f['type'], $allowed) && $f['size'] < 5*1024*1024) {
$name = time() . "_" . bin2hex(random_bytes(4)) . ".webp";
if (move_uploaded_file($f['tmp_name'], $img_dir . $name)) {
exit(json_encode(['location' => "/uploads/blog/" . $name]));
}
}
http_response_code(400); exit;
}
$status = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Безопасность: проверка CSRF (если функции нет, пропускаем)
if (function_exists('check_csrf')) {
if (!check_csrf($_POST['csrf_token'] ?? '')) die("Ошибка безопасности: токен истек.");
}
$title = trim($_POST['title']);
$content = $_POST['content'];
$cat_id = (int)$_POST['category_id'];
$errors = ContentCore::validate($title, $content);
if (empty($errors)) {
$slug = ContentCore::createSlug($title);
$stmt = $mysqli->prepare("INSERT INTO posts (author_id, category_id, title, slug, content) VALUES (?, ?, ?, ?, ?)");
$author_id = $_SESSION['user_id'] ?? 0;
$stmt->bind_param("iisss", $author_id, $cat_id, $title, $slug, $content);
if ($stmt->execute()) {
$status = 'success';
write_log("Опубликована новая статья: $title", "info");
}
} else {
if (function_exists('set_flash')) {
set_flash(implode(" ", $errors), "error");
}
}
}
$cats = $mysqli->query("SELECT * FROM categories ORDER BY name ASC");
include '../system/header.php';
?>
<style>
:root { --glass: rgba(var(--pico-card-background-color-rgb), 0.7); }
.main-editor { max-width: 1100px; margin: auto; animation: slideUp 0.4s ease; padding: 10px; }
.editor-box {
background: var(--glass);
backdrop-filter: blur(10px);
border: 1px solid var(--pico-muted-border-color);
padding: 2rem; border-radius: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.floating-badge {
position: fixed; bottom: 20px; right: 20px;
background: var(--pico-primary); color: #fff;
padding: 8px 15px; border-radius: 50px; font-size: 0.7rem; z-index: 100;
display: none; box-shadow: 0 10px 20px var(--pico-primary-background);
}
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@media (max-width: 600px) { .editor-box { padding: 1rem; } }
</style>
<div id="draft-notify" class="floating-badge"><i class="fa-solid fa-floppy-disk"></i> Черновик восстановлен из памяти</div>
<div class="main-editor">
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 2rem;">
<hgroup style="margin:0;">
<h1 style="font-size: 2.2rem; letter-spacing: -1px;">Новая запись</h1>
<p class="secondary">Студия контента LaiCMS v1.0</p>
</hgroup>
<div class="hide-mobile">
<span id="char-count" class="badge">0 слов</span>
</div>
</div>
<form method="POST" id="postForm" class="editor-box">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?? '' ?>">
<div class="grid">
<label>Заголовок статьи
<input type="text" name="title" id="p-title" placeholder="Введите название..." required>
</label>
<label>Категория
<select name="category_id">
<option value="0">Без категории</option>
<?php while($c = $cats->fetch_assoc()): ?>
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
<?php endwhile; ?>
</select>
</label>
</div>
<label>Текст публикации</label>
<textarea id="editor" name="content"></textarea>
<footer style="margin-top: 2rem; border-top: 1px solid var(--pico-muted-border-color); padding-top: 1.5rem;">
<div class="grid">
<button type="submit" class="primary" style="border-radius: 14px; font-weight: 800;">
<i class="fa-solid fa-paper-plane"></i> Опубликовать
</button>
<button type="button" onclick="history.back()" class="outline secondary" style="border-radius: 14px;">Отмена</button>
</div>
</footer>
</form>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.8.2/tinymce.min.js"></script>
<script>
const DRAFT_KEY = 'laicms_draft_v2026';
tinymce.init({
selector: '#editor',
language: 'ru',
language_url: 'https://cdn.jsdelivr.net/npm/tinymce-i18n@23.10.9/langs6/ru.js',
height: 500,
promotion: false,
branding: false,
menu: {
file: { title: 'Файл', items: 'newdocument restoredraft | preview | print ' },
edit: { title: 'Правка', items: 'undo redo | cut copy paste | selectall | searchreplace' },
view: { title: 'Вид', items: 'code | visualaid visualchars visualblocks | preview fullscreen' },
insert: { title: 'Вставка', items: 'image link media template codesample inserttable | charmap emoticons hr | anchor' },
format: { title: 'Формат', items: 'bold italic underline strikethrough | formats blockformats align | forecolor backcolor | removeformat' },
tools: { title: 'Инструменты', items: 'code wordcount' },
table: { title: 'Таблица', items: 'inserttable | cell row column | tableprops deletetable' },
},
plugins: 'advlist autolink lists link image charmap preview anchor code fullscreen media table emoticons wordcount',
toolbar: 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright | bullist numlist | link image emoticons | code fullscreen',
skin: (document.documentElement.dataset.theme === 'dark' ? 'oxide-dark' : 'oxide'),
content_css: (document.documentElement.dataset.theme === 'dark' ? 'dark' : 'default'),
images_upload_url: 'add_post.php',
setup: (editor) => {
editor.on('init', () => {
const saved = localStorage.getItem(DRAFT_KEY);
if (saved) {
editor.setContent(saved);
document.getElementById('draft-notify').style.display = 'block';
setTimeout(() => document.getElementById('draft-notify').style.display = 'none', 4000);
}
});
editor.on('keyup change', () => {
localStorage.setItem(DRAFT_KEY, editor.getContent());
const count = editor.plugins.wordcount ? editor.plugins.wordcount.getCount() : 0;
document.getElementById('char-count').innerText = count + " слов";
});
}
});
document.getElementById('postForm').onsubmit = () => {
localStorage.removeItem(DRAFT_KEY);
};
<?php if ($status === 'success'): ?>
alert("🎉 Публикация успешно завершена!");
window.location.href = "blog_manage.php";
<?php endif; ?>
</script>
<?php include '../system/footer.php'; ?>