304 lines
13 KiB
PHP
304 lines
13 KiB
PHP
<?php
|
|
session_start();
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
header('X-Content-Type-Options: nosniff');
|
|
header('X-Frame-Options: DENY');
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
|
|
if (!isset($_SESSION['authenticated'])) {
|
|
header('Location: index.php');
|
|
exit;
|
|
}
|
|
|
|
$config = require 'config.php';
|
|
$boardId = $_GET['board'] ?? 'b';
|
|
$threadId = isset($_GET['thread']) ? urldecode($_GET['thread']) : null;
|
|
|
|
|
|
|
|
try {
|
|
$db = new PDO(
|
|
"mysql:host={$config['db']['host']};dbname={$config['db']['name']}",
|
|
$config['db']['user'],
|
|
$config['db']['pass']
|
|
);
|
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
} catch (PDOException $e) {
|
|
die('Connection failed');
|
|
}
|
|
|
|
require_once 'RateLimiter.php';
|
|
require_once 'AnonymousID.php';
|
|
require_once 'MarkdownParser.php';
|
|
$rateLimiter = new RateLimiter($db);
|
|
$rateLimiter->cleanup();
|
|
|
|
$ip = $_SERVER['REMOTE_ADDR'];
|
|
$anonymousID = new AnonymousID($db, $ip, $boardId);
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$rateLimiter->isAllowed($ip)) {
|
|
$error = 'Слишком много запросов';
|
|
} else {
|
|
$message = trim($_POST['message'] ?? '');
|
|
$file = $_FILES['file'] ?? null;
|
|
$title = trim($_POST['title'] ?? '');
|
|
$description = trim($_POST['description'] ?? '');
|
|
|
|
if ($message || $title || ($file && $file['error'] === UPLOAD_ERR_OK)) {
|
|
$postId = sprintf('%06d', mt_rand(1, 999999));
|
|
|
|
$fileName = null;
|
|
$fileSize = null;
|
|
$fileType = null;
|
|
|
|
if ($file && $file['error'] === UPLOAD_ERR_OK) {
|
|
$fileExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
|
|
|
if (!in_array($fileExt, $config['allowed_types'])) {
|
|
$error = 'Неподдерживаемый тип файла';
|
|
} elseif ($file['size'] > $config['max_file_size']) {
|
|
$error = 'Файл слишком большой';
|
|
} else {
|
|
$uploadDir = $config['upload_path'];
|
|
if (!is_dir($uploadDir)) {
|
|
mkdir($uploadDir, 0755, true);
|
|
}
|
|
|
|
$fileName = $postId . '.' . $fileExt;
|
|
$filePath = $uploadDir . $fileName;
|
|
|
|
if (move_uploaded_file($file['tmp_name'], $filePath)) {
|
|
$fileSize = $file['size'];
|
|
$fileType = $fileExt;
|
|
} else {
|
|
$error = 'Ошибка загрузки файла';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($error)) {
|
|
if ($threadId) {
|
|
$anonymousId = $anonymousID->getOrCreateID();
|
|
$stmt = $db->prepare('
|
|
INSERT INTO posts (post_id, thread_id, board_id, message, file_name, file_size, file_type, ip_address, anonymous_id)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
');
|
|
$stmt->execute([$postId, $threadId, $boardId, $message, $fileName, $fileSize, $fileType, $ip, $anonymousId]);
|
|
} else {
|
|
header('Location: newthread.php?board=' . $boardId);
|
|
exit;
|
|
}
|
|
|
|
header('Location: ' . $_SERVER['REQUEST_URI']);
|
|
exit;
|
|
}
|
|
} else {
|
|
$error = 'Введите сообщение или загрузите файл';
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
$db->exec('SET NAMES utf8');
|
|
$stmt = $db->prepare('SELECT * FROM boards WHERE board_id = ?');
|
|
$stmt->execute([$boardId]);
|
|
$board = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$board) {
|
|
header('Location: index.php');
|
|
exit;
|
|
}
|
|
} catch (PDOException $e) {
|
|
die('Database error');
|
|
}
|
|
|
|
if ($threadId) {
|
|
try {
|
|
$stmt = $db->prepare('
|
|
SELECT p.*, t.title as thread_title
|
|
FROM posts p
|
|
JOIN threads t ON p.thread_id = t.thread_id
|
|
WHERE p.thread_id = ? AND p.board_id = ?
|
|
ORDER BY p.created_at ASC
|
|
');
|
|
$stmt->execute([$threadId, $boardId]);
|
|
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
if (empty($posts)) {
|
|
header('Location: board.php?board=' . urlencode($boardId));
|
|
exit;
|
|
}
|
|
} catch (PDOException $e) {
|
|
die('Database error');
|
|
}
|
|
} else {
|
|
try {
|
|
$stmt = $db->prepare('
|
|
SELECT t.*,
|
|
COUNT(p.id) as post_count,
|
|
MAX(p.created_at) as last_post_time
|
|
FROM threads t
|
|
LEFT JOIN posts p ON t.thread_id = p.thread_id
|
|
WHERE t.board_id = ?
|
|
GROUP BY t.id
|
|
ORDER BY t.updated_at DESC
|
|
LIMIT 20
|
|
');
|
|
$stmt->execute([$boardId]);
|
|
$threads = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (PDOException $e) {
|
|
die('Database error');
|
|
}
|
|
}
|
|
|
|
function formatMessage($message) {
|
|
return MarkdownParser::parse($message);
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>mkach - /<?= htmlspecialchars($boardId) ?>/</title>
|
|
<link rel="stylesheet" href="styles.css">
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1><a href="index.php" class="home-link">mkach</a> - /<?= htmlspecialchars($boardId) ?>/ - <?= htmlspecialchars($board['name']) ?></h1>
|
|
<div class="header-buttons">
|
|
<a href="index.php" class="boards-btn">Доски</a>
|
|
<a href="?logout=1" class="logout-btn">Выход</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="board-container">
|
|
<?php if (!empty($error)): ?>
|
|
<div class="error"><?= htmlspecialchars($error) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($threadId): ?>
|
|
<div class="thread-container">
|
|
<div class="thread-header">
|
|
<h2><?= htmlspecialchars($posts[0]['thread_title'] ?? 'Без названия') ?></h2>
|
|
<a href="board.php?board=<?= urlencode($boardId) ?>" class="back-link">← Назад к списку</a>
|
|
</div>
|
|
|
|
<div class="posts-container" id="posts">
|
|
<?php foreach ($posts as $post): ?>
|
|
<div class="post" id="<?= htmlspecialchars($post['post_id']) ?>">
|
|
<div class="post-header">
|
|
<span class="post-id">№<?= htmlspecialchars($post['post_id']) ?></span>
|
|
<?php if ($post['anonymous_id']): ?>
|
|
<span class="anonymous-id"><?= htmlspecialchars($post['anonymous_id']) ?></span>
|
|
<?php endif; ?>
|
|
<span class="post-time"><?= date('d.m.Y H:i:s', strtotime($post['created_at'])) ?></span>
|
|
</div>
|
|
|
|
<?php if ($post['file_name']): ?>
|
|
<div class="post-file">
|
|
<a href="uploads/<?= htmlspecialchars($post['file_name']) ?>" target="_blank">
|
|
<img src="uploads/<?= htmlspecialchars($post['file_name']) ?>" alt="File" class="post-image">
|
|
</a>
|
|
<div class="file-info">
|
|
<?= htmlspecialchars($post['file_name']) ?>
|
|
(<?= number_format($post['file_size'] / 1024, 1) ?> KB)
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($post['message']): ?>
|
|
<div class="post-message"><?= formatMessage($post['message']) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="threads-container">
|
|
<div class="threads-list" id="threads">
|
|
<?php foreach ($threads as $thread): ?>
|
|
<div class="thread-item">
|
|
<div class="thread-header">
|
|
<a href="board.php?board=<?= urlencode($boardId) ?>&thread=<?= urlencode($thread['thread_id']) ?>" class="thread-link">
|
|
<span class="thread-title"><?= htmlspecialchars($thread['title'] ?? 'Без названия') ?></span>
|
|
</a>
|
|
<span class="thread-info">
|
|
Постов: <?= $thread['post_count'] ?> |
|
|
Обновлено: <?= date('d.m.Y H:i', strtotime($thread['last_post_time'] ?? $thread['created_at'])) ?>
|
|
<?php if ($thread['anonymous_id']): ?>
|
|
| <?= htmlspecialchars($thread['anonymous_id']) ?>
|
|
<?php endif; ?>
|
|
</span>
|
|
</div>
|
|
|
|
<?php if ($thread['file_name']): ?>
|
|
<div class="thread-file">
|
|
<a href="uploads/<?= htmlspecialchars($thread['file_name']) ?>" target="_blank">
|
|
<img src="uploads/<?= htmlspecialchars($thread['file_name']) ?>" alt="File" class="thread-image">
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($thread['description']): ?>
|
|
<div class="thread-description"><?= formatMessage($thread['description']) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php if (!$threadId): ?>
|
|
<div class="new-thread-button">
|
|
<a href="newthread.php?board=<?= urlencode($boardId) ?>" class="new-thread-btn">Создать новый тред</a>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="post-form">
|
|
<form method="post" enctype="multipart/form-data">
|
|
<div class="form-row">
|
|
<textarea name="message" placeholder="Сообщение (поддерживает Markdown)" class="message-input"></textarea>
|
|
</div>
|
|
<div class="form-row">
|
|
<input type="file" name="file" accept=".jpg,.jpeg,.png,.gif,.webp" class="file-input">
|
|
<button type="submit" class="send-btn">Отправить</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<script>
|
|
let isScrolledToBottom = true;
|
|
const container = document.getElementById('posts') || document.getElementById('threads');
|
|
|
|
if (container) {
|
|
container.addEventListener('scroll', () => {
|
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
isScrolledToBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 1;
|
|
});
|
|
|
|
setInterval(() => {
|
|
fetch(window.location.href)
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
const newContainer = doc.getElementById('posts') || doc.getElementById('threads');
|
|
if (newContainer) {
|
|
const oldHeight = container.scrollHeight;
|
|
container.innerHTML = newContainer.innerHTML;
|
|
|
|
if (isScrolledToBottom) {
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
}
|
|
});
|
|
}, 10000);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|