logfile = LOGFILE;
$this->loadData();
$this->handleRequest();
}
private function loadData() {
if (!file_exists($this->logfile)) {
file_put_contents($this->logfile, '');
return;
}
$lines = file($this->logfile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$data = explode("\t", $line);
if (count($data) >= 10) {
$post = [
'id' => $data[0],
'thread' => $data[1],
'datetime' => $data[2],
'name' => $data[3],
'email' => $data[4],
'title' => $data[5],
'body' => $data[6],
'ip' => $data[7],
'delkey' => $data[8],
'markup' => $data[9] ?? 'waka'
];
if ($post['thread'] == '0') {
$this->threads[$post['id']] = $post;
}
$this->posts[] = $post;
}
}
}
private function saveData() {
$data = '';
foreach ($this->posts as $post) {
$data .= implode("\t", [
$post['id'],
$post['thread'],
$post['datetime'],
$post['name'],
$post['email'],
$post['title'],
$post['body'],
$post['ip'],
$post['delkey'],
$post['markup']
]) . "\n";
}
file_put_contents($this->logfile, $data, LOCK_EX);
}
private function handleRequest() {
$task = $_POST['task'] ?? $_GET['task'] ?? 'default';
if (NIKKI && $task == 'post') {
$this->showError('Board is closed');
return;
}
switch ($task) {
case 'post':
$this->handlePost();
break;
case 'admin':
$this->handleAdmin();
break;
default:
$this->showBoard();
break;
}
}
private function handlePost() {
if (!$this->checkRateLimit()) {
$this->showError('Please wait ' . RENZOKU . ' seconds between posts');
return;
}
$thread = $_POST['thread'] ?? '0';
$name = $this->sanitize($_POST['field_a'] ?? 'Nameless');
$email = $this->sanitize($_POST['field_b'] ?? '');
$title = $this->sanitize($_POST['title'] ?? '');
$comment = $_POST['comment'] ?? '';
$markup = $_POST['markup'] ?? MARKUP_DEFAULT;
// Spam trap check
if (!empty($_POST['name']) || !empty($_POST['link'])) {
$this->showError('Spam detected');
return;
}
if (empty($comment) && $thread == '0' && empty($title)) {
$this->showError('Comment and title cannot both be empty');
return;
}
if ($thread == '0' && empty($title)) {
$this->showError('Thread title is required');
return;
}
$ip = $_SERVER['REMOTE_ADDR'];
$delkey = $this->generateDelKey();
$id = $this->generateId();
$datetime = date('Y/m/d H:i');
// Process comment
$body = $this->processMarkup($comment, $markup);
$post = [
'id' => $id,
'thread' => $thread,
'datetime' => $datetime,
'name' => $name,
'email' => $email,
'title' => $title,
'body' => $body,
'ip' => $ip,
'delkey' => password_hash($delkey, PASSWORD_DEFAULT),
'markup' => $markup
];
$this->posts[] = $post;
if ($thread == '0') {
$this->threads[$id] = $post;
}
$this->pruneOldPosts();
$this->saveData();
$this->setRateLimit();
// Redirect to prevent double posting
header('Location: ' . PHP_SELF);
exit;
}
private function handleAdmin() {
$pass = $_POST['admin_pass'] ?? '';
$action = $_POST['admin_action'] ?? '';
if ($pass !== ADMIN_PASS) {
$this->showError('Invalid admin password');
return;
}
switch ($action) {
case 'delete':
$delete_id = $_POST['delete_id'] ?? '';
$this->deletePost($delete_id);
break;
case 'ban':
$ban_ip = $_POST['ban_ip'] ?? '';
$this->banIP($ban_ip);
break;
}
header('Location: ' . PHP_SELF);
exit;
}
private function deletePost($id) {
$this->posts = array_filter($this->posts, function($post) use ($id) {
return $post['id'] != $id;
});
if (isset($this->threads[$id])) {
unset($this->threads[$id]);
}
$this->saveData();
}
private function banIP($ip) {
$banfile = 'banned_ips.txt';
file_put_contents($banfile, $ip . "\n", FILE_APPEND | LOCK_EX);
}
private function checkBanned() {
$banfile = 'banned_ips.txt';
if (!file_exists($banfile)) return false;
$banned_ips = file($banfile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
return in_array($_SERVER['REMOTE_ADDR'], $banned_ips);
}
private function checkRateLimit() {
$ip = $_SERVER['REMOTE_ADDR'];
$last_post = $_SESSION['last_post_' . $ip] ?? 0;
return (time() - $last_post) >= RENZOKU;
}
private function setRateLimit() {
$ip = $_SERVER['REMOTE_ADDR'];
$_SESSION['last_post_' . $ip] = time();
}
private function generateId() {
return time() . rand(100, 999);
}
private function generateDelKey() {
return substr(md5(uniqid(rand(), true)), 0, 8);
}
private function sanitize($str) {
return htmlspecialchars(trim($str), ENT_QUOTES, 'UTF-8');
}
private function processMarkup($text, $markup) {
$text = $this->sanitize($text);
switch ($markup) {
case 'waka':
return $this->processWakabaMarkup($text);
case 'html':
return $this->processHTMLMarkup($text);
case 'aa':
return $this->processAAMarkup($text);
default:
return $this->processNoneMarkup($text);
}
}
private function processWakabaMarkup($text) {
// Convert line breaks
$text = nl2br($text);
// Bold **text**
$text = preg_replace('/\*\*(.*?)\*\*/', '$1', $text);
// Italic *text*
$text = preg_replace('/\*(.*?)\*/', '$1', $text);
// Greentext
$text = preg_replace('/^(>.*)$/m', '$1', $text);
// Auto-link URLs
$text = preg_replace('/https?:\/\/[^\s<>"]+/', '$0', $text);
// Quote links >>123
$text = preg_replace('/>>(\d+)/', '>>$1', $text);
return $text;
}
private function processHTMLMarkup($text) {
// Allow limited HTML tags
$allowed_tags = '
';
echo '';
return strip_tags($text, $allowed_tags);
}
private function processAAMarkup($text) {
$text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
return '
' . $text . '
';
}
private function processNoneMarkup($text) {
$text = nl2br($text);
// Auto-link URLs only
$text = preg_replace('/https?:\/\/[^\s<>"]+/', '$0', $text);
// Quote links
$text = preg_replace('/>>(\d+)/', '>>$1', $text);
return $text;
}
private function pruneOldPosts() {
if (count($this->posts) > LOG_MAX) {
$this->posts = array_slice($this->posts, -LOG_MAX);
}
// Remove old threads
if (count($this->threads) > MAX_THREADS) {
$sorted_threads = $this->threads;
uasort($sorted_threads, function($a, $b) {
return strtotime($a['datetime']) - strtotime($b['datetime']);
});
$to_remove = array_slice(array_keys($sorted_threads), 0, count($sorted_threads) - MAX_THREADS);
foreach ($to_remove as $thread_id) {
unset($this->threads[$thread_id]);
$this->posts = array_filter($this->posts, function($post) use ($thread_id) {
return $post['thread'] != $thread_id && $post['id'] != $thread_id;
});
}
}
}
private function getThreadPosts($thread_id) {
return array_filter($this->posts, function($post) use ($thread_id) {
return $post['thread'] == $thread_id || $post['id'] == $thread_id;
});
}
private function getThreadReplies($thread_id) {
return array_filter($this->posts, function($post) use ($thread_id) {
return $post['thread'] == $thread_id;
});
}
private function getLastBumpTime($thread_id) {
$posts = $this->getThreadPosts($thread_id);
$latest = 0;
foreach ($posts as $post) {
$time = strtotime($post['datetime']);
if ($time > $latest) {
$latest = $time;
}
}
return $latest;
}
private function getThreadsByBumpOrder() {
$threads_with_bump = [];
foreach ($this->threads as $thread_id => $thread) {
$threads_with_bump[$thread_id] = [
'thread' => $thread,
'bump_time' => $this->getLastBumpTime($thread_id)
];
}
// Sort by bump time (newest first)
uasort($threads_with_bump, function($a, $b) {
return $b['bump_time'] - $a['bump_time'];
});
return $threads_with_bump;
}
private function showError($message) {
$this->showHeader();
echo '= TITLE ?>