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 25aadaf..a99c000 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;
}
@@ -10458,7 +10460,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 60772bd..a32986e 100644
--- a/src/index.mjs
+++ b/src/index.mjs
@@ -79,11 +79,13 @@ const nginx502Fallback = `
`;
// Login + Register modal injected before