Merge branch 'main' into codex/implement-new-chat-age-restrictions-vvodw9

This commit is contained in:
2025-11-03 19:22:44 +01:00
committed by GitHub
+138 -305
View File
@@ -30,8 +30,6 @@ define('LOG_RETENTION_MONTHS', 6);
define('ONLINE_TIMEOUT_SECONDS', 30); define('ONLINE_TIMEOUT_SECONDS', 30);
define('SSE_RETRY_MS', 500); define('SSE_RETRY_MS', 500);
define('MAX_MESSAGES_PER_FETCH', 200); define('MAX_MESSAGES_PER_FETCH', 200);
define('MAX_ATTACHMENT_SIZE', 200 * 1024); // 200 KB
define('UPLOAD_DIR', __DIR__ . '/uploads');
// Rate Limiting // Rate Limiting
define('MAX_MESSAGES_PER_MINUTE', 10); define('MAX_MESSAGES_PER_MINUTE', 10);
@@ -73,152 +71,129 @@ $PROFANITY_FILTER = [
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
function getDB() { function getDB() {
static $db = null; $db = new SQLite3(DB_FILE);
static $initialized = false; $db->busyTimeout(5000);
// Users Table (mit Geburtsdatum und User-ID)
$db->exec('
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
user_id TEXT UNIQUE NOT NULL,
birthdate DATE NOT NULL,
age_group TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
is_banned INTEGER DEFAULT 0,
ban_reason TEXT
)
');
// Messages Table
$db->exec('
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_user_id INTEGER NOT NULL,
to_user_id INTEGER NOT NULL,
message TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
is_read INTEGER DEFAULT 0,
is_flagged INTEGER DEFAULT 0,
flag_reason TEXT,
FOREIGN KEY (from_user_id) REFERENCES users(id),
FOREIGN KEY (to_user_id) REFERENCES users(id)
)
');
// Online Status Table
$db->exec('
CREATE TABLE IF NOT EXISTS online_status (
user_id INTEGER PRIMARY KEY,
last_ping DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
');
if ($db === null) { // User Sessions Table
if (!is_dir(UPLOAD_DIR)) { $db->exec('
@mkdir(UPLOAD_DIR, 0755, true); CREATE TABLE IF NOT EXISTS user_sessions (
} user_id INTEGER PRIMARY KEY,
session_token TEXT NOT NULL,
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
');
$db = new SQLite3(DB_FILE); // Reports Table
$db->busyTimeout(5000); $db->exec('
} CREATE TABLE IF NOT EXISTS reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
if (!$initialized) { reporter_id INTEGER NOT NULL,
// Users Table (mit Geburtsdatum und User-ID) reported_user_id INTEGER NOT NULL,
$db->exec(' reason TEXT NOT NULL,
CREATE TABLE IF NOT EXISTS users ( message_id INTEGER,
id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
username TEXT NOT NULL, status TEXT DEFAULT "pending",
user_id TEXT UNIQUE NOT NULL, FOREIGN KEY (reporter_id) REFERENCES users(id),
birthdate DATE NOT NULL, FOREIGN KEY (reported_user_id) REFERENCES users(id),
age_group TEXT NOT NULL, FOREIGN KEY (message_id) REFERENCES messages(id)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, )
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP, ');
is_banned INTEGER DEFAULT 0,
ban_reason TEXT // Blocks Table
) $db->exec('
'); CREATE TABLE IF NOT EXISTS blocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
// Messages Table blocker_id INTEGER NOT NULL,
$db->exec(' blocked_id INTEGER NOT NULL,
CREATE TABLE IF NOT EXISTS messages ( timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
id INTEGER PRIMARY KEY AUTOINCREMENT, FOREIGN KEY (blocker_id) REFERENCES users(id),
from_user_id INTEGER NOT NULL, FOREIGN KEY (blocked_id) REFERENCES users(id),
to_user_id INTEGER NOT NULL, UNIQUE(blocker_id, blocked_id)
message TEXT NOT NULL, )
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, ');
is_read INTEGER DEFAULT 0,
is_flagged INTEGER DEFAULT 0, // Rate Limiting Table
flag_reason TEXT, $db->exec('
attachment_path TEXT, CREATE TABLE IF NOT EXISTS rate_limits (
attachment_type TEXT, user_id INTEGER NOT NULL,
attachment_size INTEGER, action_type TEXT NOT NULL,
FOREIGN KEY (from_user_id) REFERENCES users(id), timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (to_user_id) REFERENCES users(id) FOREIGN KEY (user_id) REFERENCES users(id)
) )
'); ');
// Ensure attachment columns exist for older installations // Logs Table (für Behörden)
$messagesInfo = $db->query('PRAGMA table_info(messages)'); $db->exec('
$messageColumns = []; CREATE TABLE IF NOT EXISTS security_logs (
while ($column = $messagesInfo->fetchArray(SQLITE3_ASSOC)) { id INTEGER PRIMARY KEY AUTOINCREMENT,
$messageColumns[$column['name']] = true; user_id INTEGER,
} action TEXT NOT NULL,
if (!isset($messageColumns['attachment_path'])) { details TEXT,
$db->exec('ALTER TABLE messages ADD COLUMN attachment_path TEXT'); ip_address TEXT,
} timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
if (!isset($messageColumns['attachment_type'])) { FOREIGN KEY (user_id) REFERENCES users(id)
$db->exec('ALTER TABLE messages ADD COLUMN attachment_type TEXT'); )
} ');
if (!isset($messageColumns['attachment_size'])) {
$db->exec('ALTER TABLE messages ADD COLUMN attachment_size INTEGER'); // Admin Table
} $db->exec('
CREATE TABLE IF NOT EXISTS admins (
// Online Status Table id INTEGER PRIMARY KEY AUTOINCREMENT,
$db->exec(' username TEXT UNIQUE NOT NULL,
CREATE TABLE IF NOT EXISTS online_status ( password_hash TEXT NOT NULL,
user_id INTEGER PRIMARY KEY, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
last_ping DATETIME DEFAULT CURRENT_TIMESTAMP, )
FOREIGN KEY (user_id) REFERENCES users(id) ');
)
'); // Create default admin if not exists
$stmt = $db->prepare('SELECT COUNT(*) as count FROM admins WHERE username = :username');
// User Sessions Table $stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT);
$db->exec(' $result = $stmt->execute();
CREATE TABLE IF NOT EXISTS user_sessions ( $row = $result->fetchArray(SQLITE3_ASSOC);
user_id INTEGER PRIMARY KEY,
session_token TEXT NOT NULL, if ($row['count'] == 0) {
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP, $stmt = $db->prepare('INSERT INTO admins (username, password_hash) VALUES (:username, :password)');
FOREIGN KEY (user_id) REFERENCES users(id)
)
');
// Reports Table
$db->exec('
CREATE TABLE IF NOT EXISTS reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reporter_id INTEGER NOT NULL,
reported_user_id INTEGER NOT NULL,
reason TEXT NOT NULL,
message_id INTEGER,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT "pending",
FOREIGN KEY (reporter_id) REFERENCES users(id),
FOREIGN KEY (reported_user_id) REFERENCES users(id),
FOREIGN KEY (message_id) REFERENCES messages(id)
)
');
// Blocks Table
$db->exec('
CREATE TABLE IF NOT EXISTS blocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
blocker_id INTEGER NOT NULL,
blocked_id INTEGER NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (blocker_id) REFERENCES users(id),
FOREIGN KEY (blocked_id) REFERENCES users(id),
UNIQUE(blocker_id, blocked_id)
)
');
// Rate Limiting Table
$db->exec('
CREATE TABLE IF NOT EXISTS rate_limits (
user_id INTEGER NOT NULL,
action_type TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
');
// Logs Table (für Behörden)
$db->exec('
CREATE TABLE IF NOT EXISTS security_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action TEXT NOT NULL,
details TEXT,
ip_address TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
');
// Admin Table
$db->exec('
CREATE TABLE IF NOT EXISTS admins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
');
// Create default admin if not exists
$stmt = $db->prepare('SELECT COUNT(*) as count FROM admins WHERE username = :username');
$stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT); $stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT);
$result = $stmt->execute(); $result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC); $row = $result->fetchArray(SQLITE3_ASSOC);
@@ -240,6 +215,14 @@ function getDB() {
$initialized = true; $initialized = true;
} }
// Create indexes
$db->exec('CREATE INDEX IF NOT EXISTS idx_messages_users ON messages(from_user_id, to_user_id)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_users_age_group ON users(age_group)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_blocks ON blocks(blocker_id, blocked_id)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_user_sessions_last_seen ON user_sessions(last_seen)');
return $db; return $db;
} }
@@ -921,9 +904,6 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
m.timestamp, m.timestamp,
m.is_read, m.is_read,
m.is_flagged, m.is_flagged,
m.attachment_path,
m.attachment_type,
m.attachment_size,
u.username as from_username, u.username as from_username,
u.user_id as from_display_id u.user_id as from_display_id
FROM messages m FROM messages m
@@ -1791,7 +1771,6 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #fef08a 0%, #f97316 100%); background: linear-gradient(135deg, #fef08a 0%, #f97316 100%);
background-color: #fff9db;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -3243,14 +3222,6 @@ const chatStateMessageEl = document.getElementById('chatStateMessage');
const chatMessagesHeaderEl = document.getElementById('chatMessagesHeader'); const chatMessagesHeaderEl = document.getElementById('chatMessagesHeader');
const chatInputEl = document.getElementById('chatInput'); const chatInputEl = document.getElementById('chatInput');
const sendButtonEl = document.getElementById('sendButton'); const sendButtonEl = document.getElementById('sendButton');
const attachmentButtonEl = document.getElementById('attachmentButton');
const attachmentInputEl = document.getElementById('attachmentInput');
const attachmentInfoEl = document.getElementById('attachmentInfo');
const attachmentFileNameEl = document.getElementById('attachmentFileName');
const attachmentClearBtnEl = document.getElementById('attachmentClearBtn');
const attachmentWarningEl = document.getElementById('attachmentWarning');
const ATTACHMENT_MAX_SIZE = 200 * 1024;
let messageAbortController = null;
async function loadUsers() { async function loadUsers() {
if (!userListEl) { if (!userListEl) {
@@ -3310,23 +3281,9 @@ function renderUserList() {
return; return;
} }
const offlineLimit = 5;
const onlineUsers = [];
const offlineUsers = [];
filtered.forEach(user => {
if (user.is_online) {
onlineUsers.push(user);
} else {
offlineUsers.push(user);
}
});
const limitedUsers = onlineUsers.concat(offlineUsers.slice(0, offlineLimit));
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
limitedUsers.forEach(user => { filtered.forEach(user => {
const item = document.createElement('button'); const item = document.createElement('button');
item.type = 'button'; item.type = 'button';
item.className = 'user-item' + (Number(user.id) === Number(state.selectedUserId) ? ' active' : ''); item.className = 'user-item' + (Number(user.id) === Number(state.selectedUserId) ? ' active' : '');
@@ -3374,15 +3331,6 @@ function renderUserList() {
userListEl.innerHTML = ''; userListEl.innerHTML = '';
userListEl.appendChild(fragment); userListEl.appendChild(fragment);
if (offlineUsers.length > offlineLimit) {
const hint = document.createElement('div');
hint.className = 'user-status';
hint.style.textAlign = 'center';
hint.style.marginTop = '12px';
hint.textContent = 'Weitere Offline-Nutzer werden ausgeblendet.';
userListEl.appendChild(hint);
}
} }
function renderChatHeader(displayName) { function renderChatHeader(displayName) {
@@ -3440,9 +3388,6 @@ function selectUser(userId, displayName) {
state.selectedUserId = userId; state.selectedUserId = userId;
state.messages = []; state.messages = [];
clearAttachmentSelection();
clearAttachmentWarning();
if (chatWelcomeEl) { if (chatWelcomeEl) {
chatWelcomeEl.style.display = 'none'; chatWelcomeEl.style.display = 'none';
} }
@@ -3467,18 +3412,11 @@ async function loadMessages(userId) {
return; return;
} }
if (messageAbortController) {
messageAbortController.abort();
}
const currentController = new AbortController();
messageAbortController = currentController;
state.isLoadingMessages = true; state.isLoadingMessages = true;
updateChatState('loading', 'Nachrichten werden geladen…'); updateChatState('loading', 'Nachrichten werden geladen…');
try { try {
const response = await fetch(`?action=get_messages&user_id=${userId}`, { signal: currentController.signal }); const response = await fetch(`?action=get_messages&user_id=${userId}`);
if (!response.ok) { if (!response.ok) {
throw new Error('NETZWERK_FEHLER'); throw new Error('NETZWERK_FEHLER');
} }
@@ -3503,9 +3441,6 @@ async function loadMessages(userId) {
updateChatState('empty', 'Noch keine Nachrichten. Starte das Gespräch!'); updateChatState('empty', 'Noch keine Nachrichten. Starte das Gespräch!');
} }
} catch (error) { } catch (error) {
if (error && error.name === 'AbortError') {
return;
}
console.error('Nachrichten konnten nicht geladen werden:', error); console.error('Nachrichten konnten nicht geladen werden:', error);
state.messages = []; state.messages = [];
if (chatMessagesEl) { if (chatMessagesEl) {
@@ -3516,10 +3451,7 @@ async function loadMessages(userId) {
: 'Nachrichten konnten nicht geladen werden. Bitte versuche es erneut.'; : 'Nachrichten konnten nicht geladen werden. Bitte versuche es erneut.';
updateChatState('error', errorMessage); updateChatState('error', errorMessage);
} finally { } finally {
if (messageAbortController === currentController) { state.isLoadingMessages = false;
messageAbortController = null;
state.isLoadingMessages = false;
}
} }
} }
@@ -3560,51 +3492,12 @@ function renderMessages() {
updateChatState(null); updateChatState(null);
} }
function clearAttachmentSelection() {
if (attachmentInputEl) {
attachmentInputEl.value = '';
}
if (attachmentInfoEl) {
attachmentInfoEl.classList.add('hidden');
}
if (attachmentFileNameEl) {
attachmentFileNameEl.textContent = '';
}
}
function showAttachmentWarning(message) {
if (attachmentWarningEl) {
attachmentWarningEl.textContent = message;
attachmentWarningEl.classList.remove('hidden');
} else {
alert(message);
}
}
function clearAttachmentWarning() {
if (attachmentWarningEl) {
attachmentWarningEl.textContent = '';
attachmentWarningEl.classList.add('hidden');
}
}
async function sendMessage() { async function sendMessage() {
if (!chatInputEl) { if (!chatInputEl) {
return; return;
} }
if (!state.selectedUserId) {
showAttachmentWarning('Bitte wähle zuerst einen Chat aus.');
return;
}
const message = chatInputEl.value.trim(); const message = chatInputEl.value.trim();
const attachmentFile = attachmentInputEl?.files?.[0] || null;
if (!message && !attachmentFile) {
showAttachmentWarning('Bitte gib eine Nachricht ein oder hänge ein JPG-Bild an.');
return;
}
clearAttachmentWarning(); clearAttachmentWarning();
@@ -3635,21 +3528,11 @@ async function sendMessage() {
formData.append('attachment', attachmentFile); formData.append('attachment', attachmentFile);
} }
try { if (result.success) {
const response = await fetch('', { method: 'POST', body: formData }); chatInputEl.value = '';
const result = await response.json(); chatInputEl.dispatchEvent(new Event('input'));
} else {
if (result.success) { alert(result.error);
chatInputEl.value = '';
chatInputEl.dispatchEvent(new Event('input'));
clearAttachmentSelection();
clearAttachmentWarning();
} else {
showAttachmentWarning(result.error || 'Nachricht konnte nicht gesendet werden.');
}
} catch (error) {
console.error('Nachricht konnte nicht gesendet werden:', error);
showAttachmentWarning('Nachricht konnte nicht gesendet werden.');
} }
} }
@@ -3732,7 +3615,7 @@ function startSSE() {
state.eventSource.close(); state.eventSource.close();
} }
setTimeout(startSSE, 500); setTimeout(startSSE, 1500);
}; };
} }
@@ -3742,56 +3625,6 @@ function escapeHtml(text) {
return div.innerHTML; return div.innerHTML;
} }
function escapeAttribute(value) {
const div = document.createElement('div');
div.textContent = value ?? '';
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
}
attachmentButtonEl?.addEventListener('click', () => {
attachmentInputEl?.click();
});
attachmentInputEl?.addEventListener('change', () => {
clearAttachmentWarning();
if (!attachmentInputEl.files || attachmentInputEl.files.length === 0) {
clearAttachmentSelection();
return;
}
const file = attachmentInputEl.files[0];
const fileType = (file.type || '').toLowerCase();
const fileName = file.name || '';
const isJpeg = /^image\/jpe?g$/.test(fileType) || /\.jpe?g$/i.test(fileName);
if (!isJpeg) {
showAttachmentWarning('Nur JPG-Bilder sind erlaubt.');
clearAttachmentSelection();
return;
}
if (file.size > ATTACHMENT_MAX_SIZE) {
showAttachmentWarning('Bild ist zu groß (max. 200 KB).');
clearAttachmentSelection();
return;
}
if (attachmentInfoEl) {
attachmentInfoEl.classList.remove('hidden');
}
if (attachmentFileNameEl) {
const sizeKb = Math.max(1, Math.round(file.size / 1024));
attachmentFileNameEl.textContent = `${file.name} (${sizeKb} KB)`;
}
});
attachmentClearBtnEl?.addEventListener('click', () => {
clearAttachmentSelection();
clearAttachmentWarning();
});
sendButtonEl?.addEventListener('click', sendMessage); sendButtonEl?.addEventListener('click', sendMessage);
chatInputEl?.addEventListener('keypress', (e) => { chatInputEl?.addEventListener('keypress', (e) => {