diff --git a/README.md b/README.md index 9f16bd9..69a6634 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ uncomment in .env # COMPOSE_PROFILES=f0ckm-nginx to enable nginx proxy with auto ## Tor Hidden Service (.onion) -Tor is bundled in `docker-compose.yml` and preconfigured with a hidden service pointing to the application (`f0ckm:1337`). +Tor is bundled in `docker-compose.yml` and preconfigured as long as COMPOSE_PROFILES=f0ckm-tor is set with a hidden service pointing to the application (`f0ckm:1337`). When running `docker compose up -d`, Tor automatically generates a `.onion` address for your node. You can find your onion address by running: @@ -90,6 +90,7 @@ cat ./f0ckm-data/tor/f0ckm_hs/hostname ``` ### Automatic Onion-Location Header + To advertise your `.onion` address to Tor Browser users visiting your clearnet site, add your `.onion` address to `config.json`: ```json @@ -97,6 +98,3 @@ To advertise your `.onion` address to Tor Browser users visiting your clearnet s "onion": "http://yourgeneratedaddress.onion" } ``` - -When set, Tor Browser users visiting the HTTPS clearweb site will see a purple **".onion available"** pill in the address bar. - diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 15c8447..9fcfd84 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -1650,6 +1650,10 @@ body.layout-modern .global-sidebar-right { opacity: 1; } +body.sidebar-right-hidden #sidebar-drag-zone { + opacity: 0.7; +} + /* Always show the handle on touch devices (no hover state available) */ @media (pointer: coarse) { #sidebar-drag-zone { @@ -3874,7 +3878,7 @@ html[theme="f0ck95d"] ._204863 { line-height: 1.7; display: flex; align-items: center; - padding: 2px 4px; + padding: 0px 5px; box-sizing: border-box; } @@ -3893,8 +3897,6 @@ html[theme="f0ck95d"] ._204863 .location::before { background-size: contain; background-repeat: no-repeat; background-position: center; - image-rendering: pixelated; - image-rendering: crisp-edges; flex-shrink: 0; } @@ -10176,7 +10178,7 @@ ol { display: flex; align-items: center; gap: 20px; - padding: 0 15px; + padding: 0 5px; flex-shrink: 0; margin-left: auto; } diff --git a/public/s/js/comments.js b/public/s/js/comments.js index e25af9d..d25eae4 100644 --- a/public/s/js/comments.js +++ b/public/s/js/comments.js @@ -1784,10 +1784,11 @@ class CommentSystem { if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) { const quoteContent = line.substring(line.indexOf('>') + 1); const quoteEmojis = window.f0ckSession?.quote_emojis === true; + const escapedQuote = quoteContent.replace(/>/g, '>'); const renderedContent = quoteEmojis - ? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n)) - : quoteContent; - return `>${renderedContent.replace(/>/g, '>')}`; + ? escapedQuote.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n)) + : escapedQuote; + return `>${renderedContent}`; } // 2. Per-line limit to prevent marked.parse recursion on single giant lines diff --git a/public/s/js/sidebar-activity.js b/public/s/js/sidebar-activity.js index 5e192d3..cab51fa 100644 --- a/public/s/js/sidebar-activity.js +++ b/public/s/js/sidebar-activity.js @@ -268,10 +268,11 @@ // Manual greentext handling — apply emoji if the user preference allows it const quoteContent = line.substring(line.indexOf('>') + 1); const quoteEmojis = window.f0ckSession?.quote_emojis === true; + const escapedQuote = quoteContent.replace(/>/g, '>'); const rendered = quoteEmojis - ? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n)) - : quoteContent; - return `>${rendered.replace(/>/g, '>')}`; + ? escapedQuote.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n)) + : escapedQuote; + return `>${rendered}`; } // Per-line limit to prevent marked.parse recursion on single giant lines diff --git a/public/s/js/user_comments.js b/public/s/js/user_comments.js index 8a9eb31..588f76f 100644 --- a/public/s/js/user_comments.js +++ b/public/s/js/user_comments.js @@ -306,7 +306,12 @@ if (!window.UserCommentSystem) { const trimmed = line.trimStart(); if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) { const quoteContent = line.substring(line.indexOf('>') + 1); - return `>${quoteContent.replace(/>/g, '>')}`; + const quoteEmojis = window.f0ckSession?.quote_emojis === true; + const escapedQuote = quoteContent.replace(/>/g, '>'); + const rendered = quoteEmojis + ? escapedQuote.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n)) + : escapedQuote; + return `>${rendered}`; } // Per-line limit diff --git a/src/inc/lib.mjs b/src/inc/lib.mjs index 45ded46..224e7a3 100644 --- a/src/inc/lib.mjs +++ b/src/inc/lib.mjs @@ -460,4 +460,15 @@ export default new class { console.error(`[ERROR REF ${errId}] ${context}:`, err); return `Internal Error. Reference: ${errId}`; } + + isOnionRequest(req) { + if (!req || !req.headers) return false; + const rawHost = req.headers['x-forwarded-host'] || req.headers['host'] || req.headers['x-forwarded-server'] || ''; + if (!rawHost) return false; + const hostStr = Array.isArray(rawHost) ? rawHost[0] : String(rawHost); + const firstHost = hostStr.split(',')[0].trim(); + const hostNoPort = firstHost.split(':')[0].trim().toLowerCase(); + return hostNoPort.endsWith('.onion'); + } }; + diff --git a/src/inc/routes/register.mjs b/src/inc/routes/register.mjs index 640bdb8..ab5a419 100644 --- a/src/inc/routes/register.mjs +++ b/src/inc/routes/register.mjs @@ -48,7 +48,7 @@ export default (router, tpl) => { return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: errorMsg })); } return res.reply({ - body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), error: errorMsg, registration_open: getRegistrationOpen() }) + body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), error: errorMsg, registration_open: getRegistrationOpen() }, req) }); } @@ -58,7 +58,7 @@ export default (router, tpl) => { return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg })); } return res.reply({ - body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), error: msg, registration_open: getRegistrationOpen() }) + body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), error: msg, registration_open: getRegistrationOpen() }, req) }); }; @@ -67,7 +67,7 @@ export default (router, tpl) => { return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: true, msg })); } return res.reply({ - body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), success: msg, registration_open: getRegistrationOpen() }) + body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), success: msg, registration_open: getRegistrationOpen() }, req) }); }; @@ -89,7 +89,7 @@ export default (router, tpl) => { } // reCAPTCHA verification (bypassed for .onion requests as Google reCAPTCHA cannot validate .onion domains) - const isOnion = req.headers['host']?.endsWith('.onion'); + const isOnion = lib.isOnionRequest(req); if (!isOnion && cfg.recaptcha?.enabled && cfg.recaptcha?.secret_key) { const rcToken = req.post['g-recaptcha-response']; if (!rcToken) return renderError("Please complete the reCAPTCHA."); diff --git a/src/index.mjs b/src/index.mjs index 9467fb6..15aedce 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -79,11 +79,13 @@ const nginx502Fallback = ` `; // Login + Register modal injected before -// This string is built at startup so it can reference cfg.recaptcha values. +// Dynamic function so reCAPTCHA can be bypassed for .onion requests const _rcEnabled = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key); const _rcSiteKey = (cfg.recaptcha && cfg.recaptcha.site_key) || ''; -const gateLoginInjection = ` +function getGateLoginInjection(req) { + const rcEnabled = _rcEnabled && !lib.isOnionRequest(req); + return `