g
This commit is contained in:
@@ -30,6 +30,8 @@
|
||||
"enable_pdf": false,
|
||||
"enable_nsfl": false,
|
||||
"enable_private_uploads": true,
|
||||
"default_upload_visibility": 0,
|
||||
"allow_user_upload_visibility": true,
|
||||
"enable_item_slugs": true,
|
||||
"nsfl_tag_id": 4,
|
||||
"allowedMimes": [
|
||||
|
||||
@@ -1508,13 +1508,20 @@ CREATE TABLE public.user_options (
|
||||
force_comment_display_mode integer DEFAULT 0,
|
||||
favorites_private boolean DEFAULT false,
|
||||
hide_fav_badge boolean DEFAULT false,
|
||||
default_upload_visibility smallint DEFAULT 0
|
||||
default_upload_visibility smallint DEFAULT 0,
|
||||
banner_file character varying(255) DEFAULT NULL::character varying,
|
||||
banner_position character varying(50) DEFAULT 'center'::character varying,
|
||||
banner_size character varying(50) DEFAULT 'cover'::character varying
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.user_options OWNER TO f0ckm;
|
||||
|
||||
COMMENT ON COLUMN public.user_options.default_upload_visibility IS 'Default upload visibility preference for user (0=public, 1=unlisted, 2=private)';
|
||||
COMMENT ON COLUMN public.user_options.banner_file IS 'Custom uploaded banner image filename, stored in public/a/';
|
||||
COMMENT ON COLUMN public.user_options.banner_position IS 'CSS background-position for the banner';
|
||||
COMMENT ON COLUMN public.user_options.banner_size IS 'CSS background-size for the banner';
|
||||
|
||||
|
||||
|
||||
--
|
||||
@@ -2964,4 +2971,9 @@ ALTER TABLE public.user_emoji_favorites OWNER TO f0ckm;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_emoji_favorites_user_id ON public.user_emoji_favorites (user_id);
|
||||
|
||||
-- User Banners
|
||||
ALTER TABLE public.user_options ADD COLUMN IF NOT EXISTS banner_file character varying(255) DEFAULT NULL;
|
||||
ALTER TABLE public.user_options ADD COLUMN IF NOT EXISTS banner_position character varying(50) DEFAULT 'center';
|
||||
ALTER TABLE public.user_options ADD COLUMN IF NOT EXISTS banner_size character varying(50) DEFAULT 'cover';
|
||||
|
||||
\unrestrict RMNKNzVQLV2ZcwmM3bmhglTot5nRoju9FmRyi3eUMfNy6iJUBfHRIgXnbrpJikG
|
||||
|
||||
@@ -1518,6 +1518,8 @@ window.initUploadForm = (selector) => {
|
||||
</div>
|
||||
`;
|
||||
|
||||
const hasVisSection = !!form.querySelector('.global-visibility-section');
|
||||
if (hasVisSection) {
|
||||
const globalVis = form.querySelector('input[name="visibility"]:checked')?.value || '0';
|
||||
const visValue = (item.visibility !== undefined && item.visibility !== '') ? item.visibility : globalVis;
|
||||
item.visibility = visValue;
|
||||
@@ -1537,6 +1539,9 @@ window.initUploadForm = (selector) => {
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
visibilitySwitch = '';
|
||||
}
|
||||
|
||||
const tagsPlaceholder = window.f0ckI18n?.upload_tags_placeholder || 'Tags...';
|
||||
const minTagsHint = shitpostMinTags > 0 ? ` (min ${shitpostMinTags})` : '';
|
||||
|
||||
@@ -355,6 +355,9 @@ export default router => {
|
||||
|
||||
// Update Default Upload Visibility preference
|
||||
group.put(/\/default_upload_visibility/, lib.loggedin, async (req, res) => {
|
||||
if (cfg.allow_user_upload_visibility === false || cfg.websrv?.allow_user_upload_visibility === false) {
|
||||
return res.json({ success: false, msg: 'Custom upload visibility is disabled by the administrator' }, 403);
|
||||
}
|
||||
const vis = parseInt(req.post.default_upload_visibility, 10);
|
||||
if (isNaN(vis) || ![0, 1, 2].includes(vis)) {
|
||||
return res.json({ success: false, msg: 'Invalid visibility option' }, 400);
|
||||
|
||||
@@ -139,12 +139,25 @@ import { getManualApproval, getMinTags, getBypassDuplicateCheck } from "../../se
|
||||
|
||||
const getTargetVisibility = (req, postVis) => {
|
||||
if (cfg.enable_private_uploads === false) return 0;
|
||||
|
||||
const sysDefault = (typeof cfg.default_upload_visibility === 'number')
|
||||
? cfg.default_upload_visibility
|
||||
: (typeof cfg.websrv?.default_upload_visibility === 'number' ? cfg.websrv.default_upload_visibility : 0);
|
||||
|
||||
const allowUserOverride = cfg.allow_user_upload_visibility !== false && cfg.websrv?.allow_user_upload_visibility !== false;
|
||||
|
||||
if (!allowUserOverride) {
|
||||
return sysDefault;
|
||||
}
|
||||
|
||||
const rawHeader = req.headers ? req.headers['x-upload-visibility'] : null;
|
||||
const val = (rawHeader || postVis || '').toString().trim().toLowerCase();
|
||||
if (val === 'private' || val === '2') return 2;
|
||||
if (val === 'unlisted' || val === '1') return 1;
|
||||
if (val === 'public' || val === '0') return 0;
|
||||
return req.session?.default_upload_visibility || 0;
|
||||
return (req.session?.default_upload_visibility !== undefined && req.session?.default_upload_visibility !== null)
|
||||
? req.session.default_upload_visibility
|
||||
: sysDefault;
|
||||
};
|
||||
|
||||
// Collect request body as buffer with debug logging
|
||||
|
||||
@@ -875,13 +875,28 @@ process.on('uncaughtException', err => {
|
||||
}
|
||||
});
|
||||
|
||||
// CSRF validation helper — used by route handlers that have already populated req.session
|
||||
// NOTE: Cannot be used in flummpress app.use() middlewares for upload/avatar bypass handlers
|
||||
// because flummpress runs ALL middlewares in parallel (Promise.all), so the session
|
||||
// middleware hasn't finished by the time these run. Those handlers validate CSRF inline.
|
||||
const validateCsrf = (req, res) => {
|
||||
// Intercept app.readBody to allow memoized reading of request body.
|
||||
// flummpress runs app.use() in parallel via Promise.all before routing/body-reading,
|
||||
// so validateCsrf needs to be able to read req.post without breaking subsequent router handler body parsing.
|
||||
const _originalReadBody = app.readBody.bind(app);
|
||||
app.readBody = async (req) => {
|
||||
if (req.post !== undefined) return req.post;
|
||||
return await _originalReadBody(req);
|
||||
};
|
||||
|
||||
// CSRF validation helper — used by route handlers and global middleware
|
||||
const validateCsrf = async (req, res) => {
|
||||
if (req.session && req.session.csrf_token) {
|
||||
const token = req.headers['x-csrf-token'] || req.body?.csrf_token || req.post?.csrf_token || req.url.qs?.csrf_token;
|
||||
let token = req.headers['x-csrf-token'] || req.body?.csrf_token || req.post?.csrf_token || req.url.qs?.csrf_token;
|
||||
|
||||
// If header/query token is missing and body is not parsed yet on a non-GET method, parse it now
|
||||
if (!token && req.post === undefined && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
|
||||
try {
|
||||
req.post = await app.readBody(req);
|
||||
token = req.post?.csrf_token;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (!token || token !== req.session.csrf_token) {
|
||||
console.error(`[CSRF] Blocked ${req.method} ${req.url.pathname} for user ${req.session.user}. Reason: ${!token ? 'Missing token' : 'Token mismatch'}`);
|
||||
res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: 'Invalid CSRF token' }));
|
||||
@@ -903,7 +918,7 @@ process.on('uncaughtException', err => {
|
||||
if (cfg.websrv.halls_enabled !== false && req.url.pathname.match(/^\/api\/v2\/admin\/halls(\/|$)/)) return;
|
||||
// User hall image upload is handled by bypass middleware below
|
||||
if (cfg.websrv.userhalls_enabled !== false && cfg.websrv.enable_userhall_image_upload !== false && req.url.pathname.match(/^\/api\/v2\/me\/halls\/[^/]+\/image$/)) return;
|
||||
if (!validateCsrf(req, res)) return;
|
||||
if (!(await validateCsrf(req, res))) return;
|
||||
});
|
||||
|
||||
// Bypass middleware for direct upload handling
|
||||
@@ -1363,6 +1378,8 @@ process.on('uncaughtException', err => {
|
||||
site_description: cfg.websrv.description || "The webs dumpster",
|
||||
enable_nsfl: !!cfg.enable_nsfl,
|
||||
enable_private_uploads: cfg.enable_private_uploads !== false,
|
||||
default_upload_visibility: (typeof cfg.default_upload_visibility === 'number' ? cfg.default_upload_visibility : (typeof cfg.websrv?.default_upload_visibility === 'number' ? cfg.websrv.default_upload_visibility : 0)),
|
||||
allow_user_upload_visibility: cfg.allow_user_upload_visibility !== false && cfg.websrv?.allow_user_upload_visibility !== false,
|
||||
nsfl_tag_id: cfg.nsfl_tag_id || 3,
|
||||
scroller_mime_cats: Array.isArray(cfg.allowedMimes) ? cfg.allowedMimes.filter(c => ['video','image','audio'].includes(c)) : ['video','image','audio'],
|
||||
themes_json: JSON.stringify(cfg.websrv.themes || []),
|
||||
|
||||
@@ -145,6 +145,15 @@ export const handleUpload = async (req, res, self) => {
|
||||
// Parse visibility: Header 'X-Upload-Visibility' or body field 'visibility' or user default preference
|
||||
let targetVisibility = 0;
|
||||
if (cfg.enable_private_uploads !== false) {
|
||||
const sysDefault = (typeof cfg.default_upload_visibility === 'number')
|
||||
? cfg.default_upload_visibility
|
||||
: (typeof cfg.websrv?.default_upload_visibility === 'number' ? cfg.websrv.default_upload_visibility : 0);
|
||||
|
||||
const allowUserOverride = cfg.allow_user_upload_visibility !== false && cfg.websrv?.allow_user_upload_visibility !== false;
|
||||
|
||||
if (!allowUserOverride) {
|
||||
targetVisibility = sysDefault;
|
||||
} else {
|
||||
const rawVisHeader = req.headers['x-upload-visibility'];
|
||||
const rawVisBody = parts.visibility;
|
||||
const visVal = (rawVisHeader || rawVisBody || '').toString().trim().toLowerCase();
|
||||
@@ -156,7 +165,10 @@ export const handleUpload = async (req, res, self) => {
|
||||
} else if (visVal === 'public' || visVal === '0') {
|
||||
targetVisibility = 0;
|
||||
} else {
|
||||
targetVisibility = req.session?.default_upload_visibility || 0;
|
||||
targetVisibility = (req.session?.default_upload_visibility !== undefined && req.session?.default_upload_visibility !== null)
|
||||
? req.session.default_upload_visibility
|
||||
: sysDefault;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,18 +117,20 @@
|
||||
status.style.color = 'var(--accent)';
|
||||
|
||||
try {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || '{{ csrf_token }}';
|
||||
const res = await fetch('/admin/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
manual_approval: approvalToggle.checked ? 'on' : 'off',
|
||||
...(registrationToggle ? { registration_open: registrationToggle.checked ? 'on' : 'off' } : {}),
|
||||
min_tags: minTagsInput.value,
|
||||
trusted_uploads: trustedUploadsInput.value,
|
||||
csrf_token: '{{ csrf_token }}'
|
||||
csrf_token: csrfToken
|
||||
}).toString()
|
||||
});
|
||||
|
||||
|
||||
@@ -334,17 +334,19 @@
|
||||
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.hide_fav_badge_hint') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
@if(allow_user_upload_visibility !== false)
|
||||
<div class="setting-item" style="margin-top: 15px;">
|
||||
<label for="default_upload_visibility_select" style="display: block; font-weight: bold; margin-bottom: 5px;">
|
||||
<span>{{ t('settings.default_upload_visibility') }}</span>
|
||||
</label>
|
||||
<select id="default_upload_visibility_select" class="form-control" style="width: 200px; padding: 5px; background: var(--bg-color); color: var(--text-color); border: 1px solid var(--nav-border-color); border-radius: 4px;">
|
||||
<option value="0" @if(session.default_upload_visibility === 0 || !session.default_upload_visibility) selected @endif>{{ t('visibility.public') }}</option>
|
||||
<option value="0" @if(session.default_upload_visibility === 0 || (!session.default_upload_visibility && session.default_upload_visibility !== 0)) selected @endif>{{ t('visibility.public') }}</option>
|
||||
<option value="1" @if(session.default_upload_visibility === 1) selected @endif>{{ t('visibility.unlisted') }}</option>
|
||||
<option value="2" @if(session.default_upload_visibility === 2) selected @endif>{{ t('visibility.private') }}</option>
|
||||
</select>
|
||||
<small class="text-muted" style="display: block; margin-top: 5px;">{{ t('settings.default_upload_visibility_hint') }}</small>
|
||||
</div>
|
||||
@endif
|
||||
<div class="setting-item" style="margin-top: 15px;">
|
||||
<label for="show_background_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
||||
<input type="checkbox" id="show_background_toggle" @if(session.show_background !==false) checked @endif>
|
||||
|
||||
@@ -79,20 +79,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(enable_private_uploads !== false)
|
||||
@if(enable_private_uploads !== false && allow_user_upload_visibility !== false)
|
||||
<div class="form-section global-visibility-section">
|
||||
<label>{{ t('upload.visibility') || 'Visibility' }}</label>
|
||||
<div class="visibility-options rating-options">
|
||||
<label class="rating-option">
|
||||
<input type="radio" name="visibility" value="0" {{ (session.default_upload_visibility === 0 || !session.default_upload_visibility) ? 'checked' : '' }}>
|
||||
<input type="radio" name="visibility" value="0" {{ ((session.default_upload_visibility !== undefined && session.default_upload_visibility !== null ? session.default_upload_visibility : default_upload_visibility) === 0) ? 'checked' : '' }}>
|
||||
<span class="rating-label sfw" style="display: flex; align-items: center; justify-content: center; gap: 6px;"><i class="fa-solid fa-globe"></i> {{ t('common.public') || 'Public' }}</span>
|
||||
</label>
|
||||
<label class="rating-option">
|
||||
<input type="radio" name="visibility" value="1" {{ session.default_upload_visibility === 1 ? 'checked' : '' }}>
|
||||
<input type="radio" name="visibility" value="1" {{ ((session.default_upload_visibility !== undefined && session.default_upload_visibility !== null ? session.default_upload_visibility : default_upload_visibility) === 1) ? 'checked' : '' }}>
|
||||
<span class="rating-label nsfw" style="display: flex; align-items: center; justify-content: center; gap: 6px;"><i class="fa-solid fa-link"></i> {{ t('common.unlisted') || 'Unlisted' }}</span>
|
||||
</label>
|
||||
<label class="rating-option">
|
||||
<input type="radio" name="visibility" value="2" {{ session.default_upload_visibility === 2 ? 'checked' : '' }}>
|
||||
<input type="radio" name="visibility" value="2" {{ ((session.default_upload_visibility !== undefined && session.default_upload_visibility !== null ? session.default_upload_visibility : default_upload_visibility) === 2) ? 'checked' : '' }}>
|
||||
<span class="rating-label nsfl" style="display: flex; align-items: center; justify-content: center; gap: 6px;"><i class="fa-solid fa-lock"></i> {{ t('common.private') || 'Private' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user