Merge branch 'main' into codex/implement-new-chat-age-restrictions-vvodw9
This commit is contained in:
@@ -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,19 +71,9 @@ $PROFANITY_FILTER = [
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
function getDB() {
|
function getDB() {
|
||||||
static $db = null;
|
|
||||||
static $initialized = false;
|
|
||||||
|
|
||||||
if ($db === null) {
|
|
||||||
if (!is_dir(UPLOAD_DIR)) {
|
|
||||||
@mkdir(UPLOAD_DIR, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$db = new SQLite3(DB_FILE);
|
$db = new SQLite3(DB_FILE);
|
||||||
$db->busyTimeout(5000);
|
$db->busyTimeout(5000);
|
||||||
}
|
|
||||||
|
|
||||||
if (!$initialized) {
|
|
||||||
// Users Table (mit Geburtsdatum und User-ID)
|
// Users Table (mit Geburtsdatum und User-ID)
|
||||||
$db->exec('
|
$db->exec('
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
@@ -112,30 +100,11 @@ function getDB() {
|
|||||||
is_read INTEGER DEFAULT 0,
|
is_read INTEGER DEFAULT 0,
|
||||||
is_flagged INTEGER DEFAULT 0,
|
is_flagged INTEGER DEFAULT 0,
|
||||||
flag_reason TEXT,
|
flag_reason TEXT,
|
||||||
attachment_path TEXT,
|
|
||||||
attachment_type TEXT,
|
|
||||||
attachment_size INTEGER,
|
|
||||||
FOREIGN KEY (from_user_id) REFERENCES users(id),
|
FOREIGN KEY (from_user_id) REFERENCES users(id),
|
||||||
FOREIGN KEY (to_user_id) REFERENCES users(id)
|
FOREIGN KEY (to_user_id) REFERENCES users(id)
|
||||||
)
|
)
|
||||||
');
|
');
|
||||||
|
|
||||||
// Ensure attachment columns exist for older installations
|
|
||||||
$messagesInfo = $db->query('PRAGMA table_info(messages)');
|
|
||||||
$messageColumns = [];
|
|
||||||
while ($column = $messagesInfo->fetchArray(SQLITE3_ASSOC)) {
|
|
||||||
$messageColumns[$column['name']] = true;
|
|
||||||
}
|
|
||||||
if (!isset($messageColumns['attachment_path'])) {
|
|
||||||
$db->exec('ALTER TABLE messages ADD COLUMN attachment_path TEXT');
|
|
||||||
}
|
|
||||||
if (!isset($messageColumns['attachment_type'])) {
|
|
||||||
$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');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Online Status Table
|
// Online Status Table
|
||||||
$db->exec('
|
$db->exec('
|
||||||
CREATE TABLE IF NOT EXISTS online_status (
|
CREATE TABLE IF NOT EXISTS online_status (
|
||||||
@@ -223,6 +192,12 @@ function getDB() {
|
|||||||
$result = $stmt->execute();
|
$result = $stmt->execute();
|
||||||
$row = $result->fetchArray(SQLITE3_ASSOC);
|
$row = $result->fetchArray(SQLITE3_ASSOC);
|
||||||
|
|
||||||
|
if ($row['count'] == 0) {
|
||||||
|
$stmt = $db->prepare('INSERT INTO admins (username, password_hash) VALUES (:username, :password)');
|
||||||
|
$stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT);
|
||||||
|
$result = $stmt->execute();
|
||||||
|
$row = $result->fetchArray(SQLITE3_ASSOC);
|
||||||
|
|
||||||
if ($row['count'] == 0) {
|
if ($row['count'] == 0) {
|
||||||
$stmt = $db->prepare('INSERT INTO admins (username, password_hash) VALUES (:username, :password)');
|
$stmt = $db->prepare('INSERT INTO admins (username, password_hash) VALUES (:username, :password)');
|
||||||
$stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT);
|
$stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT);
|
||||||
@@ -241,6 +216,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,12 +3451,9 @@ 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) {
|
|
||||||
messageAbortController = null;
|
|
||||||
state.isLoadingMessages = false;
|
state.isLoadingMessages = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function renderMessages() {
|
function renderMessages() {
|
||||||
const container = chatMessagesEl;
|
const container = chatMessagesEl;
|
||||||
@@ -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 {
|
|
||||||
const response = await fetch('', { method: 'POST', body: formData });
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
chatInputEl.value = '';
|
chatInputEl.value = '';
|
||||||
chatInputEl.dispatchEvent(new Event('input'));
|
chatInputEl.dispatchEvent(new Event('input'));
|
||||||
clearAttachmentSelection();
|
|
||||||
clearAttachmentWarning();
|
|
||||||
} else {
|
} else {
|
||||||
showAttachmentWarning(result.error || 'Nachricht konnte nicht gesendet werden.');
|
alert(result.error);
|
||||||
}
|
|
||||||
} 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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user