↩️ Назад

Категории

article.php — просмотр статьи

14.04.2026 | коды из категории: php

🔐 ЗАЩИТА:
- intval() + проверка ID
- htmlspecialchars() для всего вывода
- PDO подготовленные запросы
- CSRF-токен для комментариев
- Капча в комментариях
- escapeHtml() в JS

⚙️ ФУНКЦИИ:
- Вывод статьи с категорией
- Вывод тегов
- Комментарии с защитой от XSS
- Адаптивное изображение в hero-блоке
- Мета-теги для SEO

<?php
session_start();
require 'includes/db.php';
include 'includes/header.php';

// Приведение ID к числу
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;

if ($id <= 0) {
    http_response_code(404);
    include '404.php';
    exit;
}

try {
    $stmt = $pdo->prepare("SELECT a.*, c.name as category FROM articles a LEFT JOIN categories c ON a.category_id = c.id WHERE a.id = ?");
    $stmt->execute([$id]);
    $article = $stmt->fetch();
} catch (Exception $e) {
    error_log("DB error: " . $e->getMessage());
    http_response_code(500);
    die("Ошибка сервера");
}

if (!$article) {
    http_response_code(404);
    include '404.php';
    exit;
}

// Мета-данные (с безопасным экранированием)
$meta_title = htmlspecialchars($article['meta_title'] ?: $article['title'], ENT_QUOTES, 'UTF-8');
$meta_description = htmlspecialchars($article['meta_description'] ?: '', ENT_QUOTES, 'UTF-8');
$post_type = 'article';

// CSRF-токен для формы комментариев
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title><?= $meta_title ?> | iot_блог</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="<?= $meta_description ?>">
    <meta name="robots" content="index, follow">
    <meta property="og:title" content="<?= htmlspecialchars($article['title'], ENT_QUOTES, 'UTF-8') ?>">
    <meta property="og:description" content="<?= $meta_description ?>">
    <meta property="og:type" content="article">
    <meta property="og:url" content="https://blog.iotprof.ru/article.php?id=<?= $article['id'] ?>">
    <link rel="canonical" href="https://blog.iotprof.ru/article.php?id=<?= $article['id'] ?>">
    <link href="/css/cool_comment.css" rel="stylesheet" />
    <link rel="stylesheet" href="/css/callback.css">
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
<br/>

<div class="container" style="position: relative;">
    <!-- Парящая кнопка возврата -->
    <a href="javascript:history.back()" class="back-button" style="text-decoration:none">
        <span class="back-arrow">↩️</span>
        Назад
    </a>
    <br/>
    
    <?php include 'includes/category_block.php'; ?>

    <h1><?= htmlspecialchars($article['title'], ENT_QUOTES, 'UTF-8') ?></h1>
    <small class="date_cat_article">
        <?= date('d.m.Y', strtotime($article['created_at'])) ?> | 
        Статья из категории: <?= htmlspecialchars($article['category'] ?: 'Без категории', ENT_QUOTES, 'UTF-8') ?>
    </small>
    <br/><br/>
    
    <?php if ($article['image']): ?>
    <div class="content">
        <style>
            #heroo {
                height: 400px;
                width: 100%;
                overflow: hidden;
                position: relative;
                background: #f0f0f0;
                border-bottom: 1px solid #ccc;
            }
            #heroo img {
                width: 100%;
                height: auto;
                display: block;
                position: absolute;
                top: 0;
                left: 0;
            }
        </style>
        <div id="heroo">
            <img src="/uploads/<?= htmlspecialchars($article['image'], ENT_QUOTES, 'UTF-8') ?>" alt="<?= $meta_title ?>">
        </div>
    </div>
    <?php endif; ?>

    <!-- Основной контент статьи -->
    <div style="margin-top: 1em;">
        <?php
        // БЕЗОПАСНЫЙ вывод контента
        // Экранируем HTML-теги, чтобы предотвратить XSS
        echo nl2br(htmlspecialchars($article['content'], ENT_QUOTES, 'UTF-8'));
        ?>
    </div>

    <?php
    // Безопасный вывод тегов
    $tag_stmt = $pdo->prepare("
        SELECT t.* FROM tags t
        JOIN article_tags at ON t.id = at.tag_id
        WHERE at.article_id = ?
    ");
    $tag_stmt->execute([$id]);
    $tags = $tag_stmt->fetchAll();

    if (!empty($tags)):
    ?>
        <div class="tags">
            Теги: 
            <?php foreach ($tags as $tag): ?>
                <a href="/tag/<?= htmlspecialchars($tag['slug'], ENT_QUOTES, 'UTF-8') ?>" class="tag">
                    #<?= htmlspecialchars($tag['name'], ENT_QUOTES, 'UTF-8') ?>
                </a>
            <?php endforeach; ?>
        </div>
    <?php endif; ?>

    <br/><hr><br/>
    
    <h3>Категории:</h3>
    <?php include 'includes/category_block.php'; ?>
</div>

<!-- === Секция комментариев === -->
<div class="comments-section" id="comments">
    <h3>Комментарии</h3>

    <?php
    $commentStmt = $pdo->prepare("SELECT * FROM comments WHERE post_id = ? AND post_type = 'article' ORDER BY created_at ASC");
    $commentStmt->execute([$id]);

    if ($commentStmt->rowCount() > 0):
        while ($comment = $commentStmt->fetch()):
    ?>
        <div class="comment-item">
            <span class="comment-author"><?= htmlspecialchars($comment['author'], ENT_QUOTES, 'UTF-8') ?></span>
            <span class="comment-date"><?= date('d.m.Y H:i', strtotime($comment['created_at'])) ?></span>
            <div class="comment-text"><?= nl2br(htmlspecialchars($comment['text'], ENT_QUOTES, 'UTF-8')) ?></div>
        </div>
    <?php 
        endwhile;
    else: 
    ?>
        <p class="no-comments">Пока нет комментариев. Будьте первым!</p>
    <?php endif; ?>
</div>

<!-- Форма добавления комментария -->
<div class="cool-comment-form">
    <h3 class="form-title">Оставить комментарий</h3>
    <form method="POST" action="/comments/add_comment.php" class="comment-form">
        <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
        <input type="hidden" name="post_type" value="article">
        <input type="hidden" name="post_id" value="<?= $id ?>">

        <div class="form-grid">
            <div class="form-group nickname-group">
                <label class="input-label">Ваш ник:</label>
                <input type="text" name="author" required class="cool-input"
                       placeholder="КрутойНик123" maxlength="30"
                       value="<?= htmlspecialchars($_SESSION['comment_author'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
            </div>

            <div class="form-group captcha-group">
                <?php
                $a = rand(1, 10);
                $b = rand(1, 10);
                $_SESSION['captcha_answer'] = $a + $b;
                ?>
                <label class="input-label"><?= $a ?> + <?= $b ?> = ?</label>
                <input type="number" name="captcha" required class="cool-input"
                       placeholder="Ответ" style="width: 100px">
            </div>

            <div class="form-group comment-group">
                <label class="input-label">Ваш комментарий:</label>
                <textarea name="text" required class="cool-textarea"
                          placeholder="Оставьте комментарий..." maxlength="500"></textarea>
            </div>
        </div>

        <button type="submit" class="submit-btn">
            <span class="btn-icon">💬</span> Отправить
        </button>
    </form>
</div>

<p><a href="/">← Назад к списку статей</a></p>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const commentForm = document.querySelector('.comment-form');
    if (commentForm) {
        commentForm.addEventListener('submit', async function(e) {
            e.preventDefault();
            const submitBtn = commentForm.querySelector('.submit-btn');
            const originalText = submitBtn.innerHTML;
            try {
                submitBtn.innerHTML = '<span class="btn-icon">⏳</span> Отправка...';
                submitBtn.disabled = true;
                const response = await fetch(commentForm.action, {
                    method: 'POST',
                    body: new FormData(commentForm)
                });
                if (!response.ok) {
                    const error = await response.json();
                    throw new Error(error.message || 'Ошибка сервера');
                }
                const data = await response.json();
                if (data.success) {
                    const commentsSection = document.querySelector('.comments-section');
                    const noCommentsMsg = commentsSection.querySelector('.no-comments');
                    if (noCommentsMsg) noCommentsMsg.remove();
                    const newComment = document.createElement('div');
                    newComment.className = 'comment-item';
                    newComment.innerHTML = `
                        <span class="comment-author">${escapeHtml(data.comment.author)}</span>
                        <span class="comment-date">${escapeHtml(data.comment.date)}</span>
                        <div class="comment-text">${escapeHtml(data.comment.text)}</div>
                    `;
                    commentsSection.appendChild(newComment);
                    commentForm.reset();
                    alert('Комментарий успешно добавлен!');
                } else {
                    throw new Error(data.message);
                }
            } catch (error) {
                console.error('Error:', error);
                alert(error.message);
            } finally {
                submitBtn.innerHTML = originalText;
                submitBtn.disabled = false;
            }
        });
    }
});

function escapeHtml(str) {
    if (!str) return '';
    return str.replace(/[&<>]/g, function(m) {
        if (m === '&') return '&amp;';
        if (m === '<') return '&lt;';
        if (m === '>') return '&gt;';
        return m;
    });
}
</script>

<script>
const hero = document.getElementById('heroo');
const heroImg = hero ? hero.querySelector('img') : null;
if (heroImg) {
    let currentOffset = 0;
    let maxOffset = 0;
    let isReady = false;
    function initImage() {
        if (!heroImg.complete || !heroImg.naturalWidth) return;
        const heroHeight = hero.clientHeight;
        const heroWidth = hero.clientWidth;
        const imgWidth = heroImg.naturalWidth;
        const imgHeight = heroImg.naturalHeight;
        const scale = heroWidth / imgWidth;
        const scaledHeight = imgHeight * scale;
        heroImg.style.height = `${scaledHeight}px`;
        maxOffset = Math.max(0, scaledHeight - heroHeight);
        currentOffset = maxOffset / 2;
        heroImg.style.transform = `translateY(${-currentOffset}px)`;
        isReady = true;
    }
    initImage();
    if (!isReady) {
        heroImg.addEventListener('load', initImage);
        heroImg.addEventListener('error', () => console.error('Hero image failed to load'));
    }
    heroImg.addEventListener('wheel', (e) => {
        if (!isReady || maxOffset <= 0) return;
        e.preventDefault();
        const step = 50;
        currentOffset = Math.max(0, Math.min(maxOffset, currentOffset + (e.deltaY > 0 ? step : -step)));
        heroImg.style.transform = `translateY(${-currentOffset}px)`;
    }, { passive: false });
}
</script>

<?php include 'includes/callpack.php'; ?>
<?php include 'includes/footer.php'; ?>



Категории:

Категории

Комментарии

Пока нет комментариев. Будьте первым!

Оставить комментарий

← Назад к списку

Посетителей сегодня: 0
о блоге | карта блога

© Digital Specialist | Не являемся сотрудниками Google, Яндекса и NASA