🔐 ЗАЩИТА:
- 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 '&';
if (m === '<') return '<';
if (m === '>') return '>';
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'; ?>
Комментарии
Пока нет комментариев. Будьте первым!