From 298f12305e13d5e1460847acade1a04452e0e49b Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Fri, 17 Jul 2026 19:36:12 +0200 Subject: [PATCH] =?UTF-8?q?dynamic=20k=C3=B6pfe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/s/css/f0ckm.css | 1 - src/inc/routes/admin.mjs | 40 ++++++++++++++ src/index.mjs | 20 ++++++- src/koepfe_handler.mjs | 92 ++++++++++++++++++++++++++++++++ views/admin.html | 1 + views/admin/koepfe.html | 110 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 src/koepfe_handler.mjs create mode 100644 views/admin/koepfe.html diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 7b67ce7..0025501 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -13482,7 +13482,6 @@ span.gchat-post-card--loading { z-index: -1; max-height: 200px; opacity: 0; - transition: opacity 0.3s ease; user-select: none; -webkit-user-drag: none; will-change: opacity; diff --git a/src/inc/routes/admin.mjs b/src/inc/routes/admin.mjs index 27c4eb3..f2e0586 100644 --- a/src/inc/routes/admin.mjs +++ b/src/inc/routes/admin.mjs @@ -1626,5 +1626,45 @@ export default (router, tpl) => { }); }); + // Koepfe Manager + router.get(/^\/admin\/koepfe\/?$/, lib.auth, async (req, res) => { + res.reply({ + body: tpl.render('admin/koepfe', { session: req.session, tmp: null }, req) + }); + }); + + router.get(/^\/api\/v2\/admin\/koepfe\/?$/, lib.auth, async (req, res) => { + try { + const fs = (await import('fs')).promises; + const path = await import('path'); + const kDir = cfg.paths.koepfe; + const files = await fs.readdir(kDir).catch(() => []); + const koepfeFiles = files.filter(f => /\.(png|jpe?g|gif|webp|avif)$/i.test(f)).map(f => ({ + filename: f, + url: '/s/koepfe/' + f + })); + if (res.json) return res.json({ koepfe: koepfeFiles }); + return res.writeHead(200, {'Content-Type': 'application/json'}).end(JSON.stringify({ koepfe: koepfeFiles })); + } catch (e) { + if (res.json) return res.json({ success: false, msg: e.message }); + return res.writeHead(500, {'Content-Type': 'application/json'}).end(JSON.stringify({ success: false, msg: e.message })); + } + }); + + router.post(/^\/api\/v2\/admin\/koepfe\/delete\/?$/, lib.auth, async (req, res) => { + try { + const { filename } = req.post || req.body || {}; + if (!filename || filename.includes('..') || filename.includes('/')) throw new Error('Invalid filename'); + const fs = (await import('fs')).promises; + const path = await import('path'); + await fs.unlink(path.join(cfg.paths.koepfe, filename)); + if (res.json) return res.json({ success: true }); + return res.writeHead(200, {'Content-Type': 'application/json'}).end(JSON.stringify({ success: true })); + } catch (e) { + if (res.json) return res.json({ success: false, msg: e.message }); + return res.writeHead(500, {'Content-Type': 'application/json'}).end(JSON.stringify({ success: false, msg: e.message })); + } + }); + return router; } diff --git a/src/index.mjs b/src/index.mjs index e2e38cf..90130f1 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -901,6 +901,15 @@ process.on('uncaughtException', err => { } }); + // Bypass middleware for koepfe uploads + app.use(async (req, res) => { + if (req.method === 'POST' && req.url.pathname === '/api/v2/admin/koepfe/upload') { + const { handleKoepfeUpload } = await import('./koepfe_handler.mjs'); + await handleKoepfeUpload(req, res); + req.url.pathname = '/handled_koepfe_upload_bypass'; + } + }); + // Bypass middleware for hall image uploads (multipart — needs raw body) app.use(async (req, res) => { if (cfg.websrv.halls_enabled === false) return; @@ -1268,7 +1277,16 @@ process.on('uncaughtException', err => { enable_item_title: cfg.websrv.enable_item_title !== false, enable_global_chat: !!cfg.websrv.enable_global_chat, embed_youtube_in_comments: cfg.websrv.embed_youtube_in_comments !== false, - koepfe_json: JSON.stringify(cfg.websrv.koepfe || []), + get koepfe_json() { + try { + const kDir = cfg.paths.koepfe; + if (!fs.existsSync(kDir)) return JSON.stringify(cfg.websrv.koepfe || []); + const files = fs.readdirSync(kDir).filter(f => /\.(png|jpe?g|gif|webp|avif)$/i.test(f)).map(f => '/s/koepfe/' + f); + return JSON.stringify(files.length ? files : (cfg.websrv.koepfe || [])); + } catch (e) { + return JSON.stringify(cfg.websrv.koepfe || []); + } + }, custom_brand_images_json: JSON.stringify(cfg.websrv.custom_brand_image || []), allowed_comment_images: cfg.websrv.allowed_comment_images || [], allowed_comment_images_json: JSON.stringify(cfg.websrv.allowed_comment_images || []), diff --git a/src/koepfe_handler.mjs b/src/koepfe_handler.mjs new file mode 100644 index 0000000..0ec6a93 --- /dev/null +++ b/src/koepfe_handler.mjs @@ -0,0 +1,92 @@ +import fs from 'fs/promises'; +import path from 'path'; +import crypto from 'crypto'; +import lib from './inc/lib.mjs'; +import cfg from './inc/config.mjs'; + +function parseMultipart(req) { + return new Promise((resolve, reject) => { + let body = []; + req.on('data', chunk => body.push(chunk)); + req.on('end', () => { + const buffer = Buffer.concat(body); + const boundaryMatch = req.headers['content-type']?.match(/boundary=(.+)$/i); + if (!boundaryMatch) return reject(new Error('No boundary')); + const boundary = '--' + boundaryMatch[1]; + let start = buffer.indexOf(boundary) + boundary.length + 2; + let fileData = null; + let filename = null; + let csrfToken = null; + + while (start < buffer.length) { + const headerEnd = buffer.indexOf('\r\n\r\n', start); + if (headerEnd === -1) break; + const headerStr = buffer.slice(start, headerEnd).toString(); + const nextBoundary = buffer.indexOf(boundary, headerEnd); + if (nextBoundary === -1) break; + const chunkData = buffer.slice(headerEnd + 4, nextBoundary - 2); + + if (headerStr.includes('name="file"')) { + const fnMatch = headerStr.match(/filename="(.+?)"/); + if (fnMatch) filename = fnMatch[1]; + fileData = chunkData; + } else if (headerStr.includes('name="csrf_token"')) { + csrfToken = chunkData.toString(); + } + start = nextBoundary + boundary.length + 2; + } + resolve({ fileData, filename, csrfToken }); + }); + req.on('error', reject); + }); +} + +import db from './inc/sql.mjs'; + +export async function handleKoepfeUpload(req, res) { + // Manual Session Lookup since this is a bypass middleware + let user = []; + if (req.cookies && req.cookies.session) { + user = await db` + select "user".id, "user".login, "user".user, "user".admin, "user_sessions".csrf_token + from "user_sessions" + left join "user" on "user".id = "user_sessions".user_id + where "user_sessions".session = ${lib.sha256(req.cookies.session)} + limit 1 + `; + } + + if (user.length === 0 || !user[0].admin) { + return res.reply ? res.reply({ code: 403, body: JSON.stringify({ success: false, msg: 'Forbidden' }) }) : res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: 'Forbidden' })); + } + + req.session = user[0]; + + try { + const { fileData, filename, csrfToken } = await parseMultipart(req); + + if (!req.session.csrf_token || csrfToken !== req.session.csrf_token) { + return res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: 'Invalid CSRF' })); + } + + if (!fileData || fileData.length === 0) { + throw new Error('No file data'); + } + + const ext = path.extname(filename || '.png').toLowerCase(); + if (!['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif'].includes(ext)) { + throw new Error('Unsupported format'); + } + + const newName = crypto.randomBytes(8).toString('hex') + ext; + const targetPath = path.join(cfg.paths.koepfe, newName); + + await fs.writeFile(targetPath, fileData); + + return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: true, file: '/s/koepfe/' + newName })); + + } catch (e) { + console.error(e); + return res.writeHead(500, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: e.message })); + } +} diff --git a/views/admin.html b/views/admin.html index dc3b3db..04f0edd 100644 --- a/views/admin.html +++ b/views/admin.html @@ -18,6 +18,7 @@
  • User Manager
  • Emoji Manager
  • Meme Manager
  • +
  • Köpfe Manager
  • Hall Manager
  • MOTD Manager
  • Wordfilter Manager
  • diff --git a/views/admin/koepfe.html b/views/admin/koepfe.html new file mode 100644 index 0000000..f1e0bf8 --- /dev/null +++ b/views/admin/koepfe.html @@ -0,0 +1,110 @@ +@include(snippets/header) +
    +
    +
    +

    Köpfe Manager

    + +
    +

    Upload Neuer Kopf

    +
    +
    + + +
    + +
    +
    + +
    +
    + + +
    +
    +@include(snippets/footer)