View file blog/add_post.php

File size: 9.67Kb
<?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'; ?>