Files
MyKeyser/forum.php
T
2026-06-19 12:03:13 -04:00

267 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__.'/includes/boot.php';
$activeNav = 'forum';
$pageTitle = 'Community Forum - Keyser, WV';
if (setting('forum_enabled','1') !== '1' && !isAdmin()) {
flash('The community forum is currently offline.', 'warning');
go('/index.php');
}
requireLogin('/forum');
$topicSlug = gs('topic');
$path = (string)(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? '');
if ($topicSlug === '' && preg_match('#^/forum/([^/]+)/?$#', $path, $m)) {
$topicSlug = urldecode($m[1]);
}
function forumAllowedExts(): array {
return uploadAllowedExts(setting('forum_allowed_filetypes','jpg,jpeg,png,gif,webp,pdf,txt,doc,docx'), ['jpg','jpeg','png','gif','webp','pdf','txt','doc','docx']);
}
function forumUploadOpts(): array {
return [
'resize_over_bytes' => mbToBytes(setting('forum_image_resize_threshold_mb','1'), 1),
'image_max_width' => (int)setting('forum_image_max_width','1600'),
'image_quality' => (int)setting('forum_image_quality','82'),
];
}
function forumStoreAttachments(): array {
return storeMultipleUploads(
'attachments',
'forum',
forumAllowedExts(),
mbToBytes(setting('forum_upload_max_mb','5'), 5),
forumUploadOpts()
);
}
function forumSaveAttachments(array $files, int $userId, int $topicId = 0, int $replyId = 0): void {
foreach ($files as $file) {
qrun(
"INSERT INTO forum_attachments(topic_id,reply_id,user_id,file_path,original_name,mime_type,file_size)VALUES(?,?,?,?,?,?,?)",
[$topicId ?: null, $replyId ?: null, $userId, $file['path'], $file['name'] ?? '', $file['mime'] ?? '', (int)($file['size'] ?? 0)]
);
}
}
function forumAttachmentList(array $attachments): string {
if (!$attachments) return '';
$out = '<div class="forum-attachments">';
foreach ($attachments as $att) {
$name = $att['original_name'] ?: basename((string)$att['file_path']);
if (isImagePath((string)$att['file_path'])) {
$out .= '<a href="'.e($att['file_path']).'" class="forum-attachment-image" target="_blank" rel="noopener"><img src="'.e($att['file_path']).'" alt="'.e($name).'"></a>';
} else {
$out .= '<a href="'.e($att['file_path']).'" class="forum-file" target="_blank" rel="noopener"><span>'.e($name).'</span><em>'.formatBytes((int)$att['file_size']).'</em></a>';
}
}
return $out.'</div>';
}
function forumText(string $text): string {
return nl2br(e($text));
}
$errors = [];
$allowedHint = implode(', ', forumAllowedExts());
$maxHint = setting('forum_upload_max_mb','5').' MB';
if ($topicSlug === '') {
if (isPost() && ps('_act') === 'create_topic') {
csrfCheck();
$title = ps('title');
$body = ps('body');
$catId = (int)p('category_id');
$cat = qone("SELECT * FROM forum_categories WHERE id=? AND is_active=1", [$catId]);
if ($title === '') $errors[] = 'Topic title is required.';
if ($body === '') $errors[] = 'Topic body is required.';
if (!$cat) $errors[] = 'Please choose a forum category.';
$uploaded = ['ok'=>true, 'files'=>[]];
if (!$errors) {
$uploaded = forumStoreAttachments();
if (!$uploaded['ok']) $errors[] = $uploaded['error'];
}
if (!$errors) {
$tempSlug = 'draft-'.bin2hex(random_bytes(8));
$topicId = qrun(
"INSERT INTO forum_topics(category_id,user_id,title,slug,body)VALUES(?,?,?,?,?)",
[$catId, uid(), $title, $tempSlug, $body]
);
$slug = uniqueForumSlug($title, $topicId);
qrun("UPDATE forum_topics SET slug=? WHERE id=?", [$slug, $topicId]);
forumSaveAttachments($uploaded['files'] ?? [], uid(), $topicId, 0);
flash('Topic posted.', 'success');
go(forumTopicUrl(['slug'=>$slug]));
}
}
$categories = qall("SELECT * FROM forum_categories WHERE is_active=1 ORDER BY sort_order,name");
$topics = qall("
SELECT ft.*, fc.name category_name, u.username,
(SELECT COUNT(*) FROM forum_replies fr WHERE fr.topic_id=ft.id) AS reply_count,
COALESCE((SELECT MAX(fr.created_at) FROM forum_replies fr WHERE fr.topic_id=ft.id), ft.created_at) AS last_activity
FROM forum_topics ft
JOIN forum_categories fc ON fc.id=ft.category_id
JOIN users u ON u.id=ft.user_id
ORDER BY ft.is_pinned DESC, datetime(last_activity) DESC
");
include __DIR__.'/includes/header.php';
?>
<div class="page-hdr">
<div class="page-hdr-inner" style="display:flex;justify-content:space-between;align-items:flex-end;gap:1rem;flex-wrap:wrap">
<div>
<div class="eyebrow">COMMUNITY FORUM</div>
<h1 style="font-size:clamp(2rem,4vw,2.9rem);margin-bottom:.4rem">Keyser Community Forum</h1>
<p style="color:var(--text-m)">Topics and replies from registered members and business owners.</p>
</div>
<?php if(setting('forum_enabled','1') !== '1' && isAdmin()): ?><span class="badge badge-gold">Disabled for non-admins</span><?php endif; ?>
</div>
</div>
<div class="section forum-layout">
<div>
<div class="section-head">
<div>
<div class="eyebrow">LATEST TOPICS</div>
<h2 class="sec-title">Conversations</h2>
</div>
</div>
<?php if($topics): ?>
<div class="forum-topic-list">
<?php foreach($topics as $topic): ?>
<a href="<?=e(forumTopicUrl($topic))?>" class="forum-topic-row">
<span class="forum-topic-main">
<strong><?=e($topic['title'])?></strong>
<em><?=e($topic['category_name'])?> by <?=e($topic['username'])?> · <?=ago($topic['last_activity'])?></em>
</span>
<span class="forum-topic-count"><?=$topic['reply_count']?> repl<?=$topic['reply_count']==1?'y':'ies'?></span>
</a>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty"><div class="empty-ico">+</div><h3>No topics yet</h3><p style="margin-top:.5rem">Start the first community conversation.</p></div>
<?php endif; ?>
</div>
<aside>
<div class="ibox" style="border-color:var(--gold-d)">
<div class="ibox-title">START A TOPIC</div>
<?php if($errors): ?><div class="err-box"><?=implode('<br>',array_map('e',$errors))?></div><?php endif; ?>
<?php if($categories): ?>
<form method="POST" enctype="multipart/form-data">
<?=csrfField()?><input type="hidden" name="_act" value="create_topic">
<div class="fg"><label class="flabel">CATEGORY *</label><select name="category_id" class="fselect" required><option value="">Choose category</option><?php foreach($categories as $cat): ?><option value="<?=$cat['id']?>"<?=((int)($_POST['category_id']??0)===(int)$cat['id'])?' selected':''?>><?=e($cat['name'])?></option><?php endforeach; ?></select></div>
<div class="fg"><label class="flabel">TITLE *</label><input type="text" name="title" class="finput" value="<?=e($_POST['title']??'')?>" required></div>
<div class="fg"><label class="flabel">BODY *</label><textarea name="body" class="ftextarea" required><?=e($_POST['body']??'')?></textarea></div>
<div class="fg"><label class="flabel">ATTACH FILES</label><input type="file" name="attachments[]" class="finput" multiple><div class="fhint">Allowed: <?=e($allowedHint)?>. Max <?=e($maxHint)?> each.</div></div>
<button class="btn btn-primary btn-block">Post Topic</button>
</form>
<?php else: ?>
<p style="color:var(--text-m)">No active categories are available yet.</p>
<?php endif; ?>
</div>
</aside>
</div>
<?php
include __DIR__.'/includes/footer.php';
exit;
}
$topic = qone("
SELECT ft.*, fc.name category_name, u.username
FROM forum_topics ft
JOIN forum_categories fc ON fc.id=ft.category_id
JOIN users u ON u.id=ft.user_id
WHERE ft.slug=?
", [$topicSlug]);
if (!$topic) { http_response_code(404); flash('Forum topic not found.', 'error'); go('/forum'); }
if (isPost() && ps('_act') === 'reply') {
csrfCheck();
if ((int)$topic['is_locked'] === 1 && !isAdmin()) {
$errors[] = 'This topic is locked.';
} else {
$body = ps('body');
$hasFile = false;
foreach (normalizeUploadFiles('attachments') as $file) {
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) $hasFile = true;
}
if ($body === '' && !$hasFile) $errors[] = 'Add a reply or attach a file.';
$uploaded = ['ok'=>true, 'files'=>[]];
if (!$errors) {
$uploaded = forumStoreAttachments();
if (!$uploaded['ok']) $errors[] = $uploaded['error'];
}
if (!$errors) {
$replyId = qrun("INSERT INTO forum_replies(topic_id,user_id,body)VALUES(?,?,?)", [$topic['id'], uid(), $body]);
forumSaveAttachments($uploaded['files'] ?? [], uid(), 0, $replyId);
qrun("UPDATE forum_topics SET updated_at=datetime('now') WHERE id=?", [$topic['id']]);
flash('Reply posted.', 'success');
go(forumTopicUrl($topic).'#reply-'.$replyId);
}
}
}
$topicAttachments = qall("SELECT * FROM forum_attachments WHERE topic_id=? AND reply_id IS NULL ORDER BY created_at", [$topic['id']]);
$replies = qall("
SELECT fr.*, u.username
FROM forum_replies fr
JOIN users u ON u.id=fr.user_id
WHERE fr.topic_id=?
ORDER BY fr.created_at ASC
", [$topic['id']]);
$replyAttachments = [];
foreach ($replies as $reply) {
$replyAttachments[$reply['id']] = qall("SELECT * FROM forum_attachments WHERE reply_id=? ORDER BY created_at", [$reply['id']]);
}
include __DIR__.'/includes/header.php';
?>
<div class="page-hdr">
<div class="page-hdr-inner">
<div class="breadcrumb"><a href="/index.php">Home</a><span></span><a href="/forum">Forum</a><span></span><?=e($topic['title'])?></div>
<div class="cat-badge"><?=e($topic['category_name'])?></div>
<h1 style="font-size:clamp(2rem,4vw,2.9rem);margin:.45rem 0"><?=e($topic['title'])?></h1>
<div class="forum-byline">Posted by <?=e($topic['username'])?> · <?=ago($topic['created_at'])?><?php if($topic['is_locked']): ?> · <span class="badge badge-gray">Locked</span><?php endif; ?></div>
</div>
</div>
<div class="section-sm forum-thread">
<article class="forum-post">
<div class="forum-post-body"><?=forumText($topic['body'])?></div>
<?=forumAttachmentList($topicAttachments)?>
</article>
<div class="forum-replies-head">
<div class="eyebrow">REPLIES</div>
<h2 class="sec-title"><?=count($replies)?> repl<?=count($replies)===1?'y':'ies'?></h2>
</div>
<?php foreach($replies as $reply): ?>
<article class="forum-reply" id="reply-<?=$reply['id']?>">
<div class="forum-reply-meta">
<div class="forum-reply-author"><span><?=e($reply['username'])?></span></div>
<a href="<?=e(forumTopicUrl($topic).'#reply-'.$reply['id'])?>" class="forum-permalink">#reply-<?=$reply['id']?></a>
</div>
<?php if($reply['body'] !== ''): ?><div class="forum-post-body"><?=forumText($reply['body'])?></div><?php endif; ?>
<?=forumAttachmentList($replyAttachments[$reply['id']] ?? [])?>
</article>
<?php endforeach; ?>
<div class="ibox" style="border-color:var(--gold-d)">
<div class="ibox-title">POST A REPLY</div>
<?php if($errors): ?><div class="err-box"><?=implode('<br>',array_map('e',$errors))?></div><?php endif; ?>
<?php if((int)$topic['is_locked'] === 1 && !isAdmin()): ?>
<p style="color:var(--text-m)">This topic is locked.</p>
<?php else: ?>
<form method="POST" enctype="multipart/form-data">
<?=csrfField()?><input type="hidden" name="_act" value="reply">
<div class="fg"><label class="flabel">REPLY</label><textarea name="body" class="ftextarea"><?=e($_POST['body']??'')?></textarea></div>
<div class="fg"><label class="flabel">ATTACH FILES</label><input type="file" name="attachments[]" class="finput" multiple><div class="fhint">Allowed: <?=e($allowedHint)?>. Max <?=e($maxHint)?> each.</div></div>
<button class="btn btn-primary">Post Reply</button>
</form>
<?php endif; ?>
</div>
</div>
<?php include __DIR__.'/includes/footer.php'; ?>