add support for uploading archives
This commit is contained in:
@@ -121,6 +121,8 @@
|
||||
"user_alternative_steuerung": false,
|
||||
"enable_swf": false,
|
||||
"swf_thumb": "/s/img/swf.png",
|
||||
"enable_archive": true,
|
||||
"archive_thumb": "/s/img/archive.webp",
|
||||
"enable_item_title": true,
|
||||
"open_registration": true,
|
||||
"open_registration_web_toggle": false,
|
||||
@@ -205,7 +207,16 @@
|
||||
"video/x-matroska": "mkv",
|
||||
"application/x-shockwave-flash": "swf",
|
||||
"application/vnd.adobe.flash.movie": "swf",
|
||||
"application/pdf": "pdf"
|
||||
"application/pdf": "pdf",
|
||||
"application/zip": "zip",
|
||||
"application/x-zip-compressed": "zip",
|
||||
"application/x-rar-compressed": "rar",
|
||||
"application/vnd.rar": "rar",
|
||||
"application/x-7z-compressed": "7z",
|
||||
"application/x-tar": "tar",
|
||||
"application/gzip": "gz",
|
||||
"application/x-bzip2": "bz2",
|
||||
"application/x-xz": "xz"
|
||||
},
|
||||
"apis": {},
|
||||
"smtp": {
|
||||
|
||||
@@ -15907,8 +15907,6 @@ body.scroller-active #gchat-reopen-bubble {
|
||||
ARCHIVE DOWNLOAD VIEW
|
||||
============================================= */
|
||||
.archive-download-view {
|
||||
background: linear-gradient(135deg, #0d1a0d 0%, #111a11 50%, #0a140a 100%) !important;
|
||||
border: 1px solid rgba(102, 187, 106, 0.15) !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
@@ -15928,15 +15926,8 @@ body.scroller-active #gchat-reopen-bubble {
|
||||
|
||||
.archive-download-icon {
|
||||
font-size: 64px;
|
||||
color: #66bb6a;
|
||||
color: var(--accent);
|
||||
opacity: 0.9;
|
||||
text-shadow: 0 0 30px rgba(102, 187, 106, 0.4), 0 0 60px rgba(102, 187, 106, 0.15);
|
||||
animation: archive-icon-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes archive-icon-pulse {
|
||||
0%, 100% { text-shadow: 0 0 20px rgba(102, 187, 106, 0.3), 0 0 40px rgba(102, 187, 106, 0.1); }
|
||||
50% { text-shadow: 0 0 35px rgba(102, 187, 106, 0.6), 0 0 70px rgba(102, 187, 106, 0.25); }
|
||||
}
|
||||
|
||||
.archive-download-filename {
|
||||
@@ -15947,9 +15938,8 @@ body.scroller-active #gchat-reopen-bubble {
|
||||
overflow-wrap: anywhere;
|
||||
max-width: 100%;
|
||||
line-height: 1.4;
|
||||
padding: 0 8px;
|
||||
background: rgba(102, 187, 106, 0.06);
|
||||
border: 1px solid rgba(102, 187, 106, 0.12);
|
||||
background: color-mix(in srgb, var(--accent) 8%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 15%, transparent);
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
font-family: monospace;
|
||||
@@ -15968,30 +15958,18 @@ body.scroller-active #gchat-reopen-bubble {
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 32px;
|
||||
background: linear-gradient(135deg, #388e3c, #2e7d32);
|
||||
color: #fff !important;
|
||||
background: var(--accent);
|
||||
color: var(--black);
|
||||
font-weight: 700;
|
||||
font-size: 1em;
|
||||
text-decoration: none !important;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(102, 187, 106, 0.3);
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 50%, transparent);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.15s, box-shadow 0.2s;
|
||||
box-shadow: 0 4px 16px rgba(56, 142, 60, 0.3);
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.archive-download-btn:hover {
|
||||
background: linear-gradient(135deg, #43a047, #388e3c);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(56, 142, 60, 0.45);
|
||||
}
|
||||
|
||||
.archive-download-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(56, 142, 60, 0.3);
|
||||
}
|
||||
|
||||
.archive-download-btn i {
|
||||
font-size: 1em;
|
||||
|
||||
BIN
public/s/img/archive.webp
Normal file
BIN
public/s/img/archive.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -807,6 +807,35 @@ window.initUploadForm = (selector) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reject archives when archive uploads are disabled
|
||||
const archiveEnabled = form.getAttribute('data-enable-archive') !== '0';
|
||||
if (!archiveEnabled) {
|
||||
const archiveExts = new Set();
|
||||
try {
|
||||
const mimesObj = JSON.parse(mimesSource || '{}');
|
||||
for (const [mime, ext] of Object.entries(mimesObj)) {
|
||||
if (mime.startsWith('application/') && ext !== 'swf' && ext !== 'pdf') {
|
||||
archiveExts.add(ext);
|
||||
}
|
||||
}
|
||||
} catch(e) { /* ignore */ }
|
||||
|
||||
const isArchiveFile = archiveExts.has(fileExt) ||
|
||||
(file.type && file.type.startsWith('application/') &&
|
||||
file.type !== 'application/x-shockwave-flash' &&
|
||||
file.type !== 'application/vnd.adobe.flash.movie' &&
|
||||
file.type !== 'application/pdf');
|
||||
|
||||
if (isArchiveFile) {
|
||||
const label = file.type || ('.' + fileExt);
|
||||
const errorMsg = `${label} uploads are disabled.`;
|
||||
if (typeof window.flashMessage === 'function') window.flashMessage('✕ ' + errorMsg, 4000, 'error');
|
||||
else if (window.showFlash) window.showFlash(errorMsg, 'error');
|
||||
else if (statusDiv) { statusDiv.textContent = errorMsg; statusDiv.className = 'upload-status error'; }
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const mimeOk = !file.type || allowedMimes.includes(file.type);
|
||||
const extOk = allowedExts.length > 0 && allowedExts.includes(fileExt);
|
||||
|
||||
|
||||
@@ -560,8 +560,37 @@ export default new class queue {
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (mime.startsWith('application/') && cfg.mimes[mime] && !['swf', 'pdf'].includes(cfg.mimes[mime])) {
|
||||
// Any application/* MIME registered in config that isn't swf or pdf is treated as an archive.
|
||||
// Both the detection and the label come straight from cfg.mimes — no hardcoded list needed.
|
||||
let customThumb = cfg.websrv && cfg.websrv.archive_thumb;
|
||||
if (customThumb && customThumb.startsWith('/')) {
|
||||
customThumb = path.join(path.resolve(), 'public', customThumb);
|
||||
}
|
||||
let usedCustom = false;
|
||||
if (customThumb) {
|
||||
try {
|
||||
const stat = await fs.promises.stat(customThumb).catch(() => null);
|
||||
if (stat && stat.size > 0) {
|
||||
await this.spawn('magick', [customThumb, tmpFile]);
|
||||
usedCustom = true;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (!usedCustom) {
|
||||
const archiveLabel = (cfg.mimes[mime] || 'arc').toUpperCase();
|
||||
await this.spawn('magick', [
|
||||
'-size', thumbSpec, 'xc:#1a2e1a',
|
||||
'-gravity', 'center',
|
||||
'-fill', '#66bb6a',
|
||||
'-pointsize', '48',
|
||||
'-annotate', '0', archiveLabel,
|
||||
tmpFile
|
||||
]).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Determine if we should use a checkerboard background for transparency
|
||||
const isTransparentMime = mime === 'image/png' || mime === 'image/webp' || mime === 'image/avif' || mime === 'image/gif';
|
||||
if (isTransparentMime) {
|
||||
// Build a grey/white checkerboard via explicit xc: squares (no pattern replacement tricks):
|
||||
|
||||
@@ -844,7 +844,8 @@ export default {
|
||||
is_repost: actitem.checksum ? actitem.checksum.includes('_bypass_') : false,
|
||||
reposts: repostItems,
|
||||
width: actitem.width || null,
|
||||
height: actitem.height || null
|
||||
height: actitem.height || null,
|
||||
original_filename: actitem.original_filename || null
|
||||
},
|
||||
title: `${actitem.id} - ${cfg.websrv.domain}`,
|
||||
pagination: {
|
||||
|
||||
@@ -129,7 +129,7 @@ export default (router, tpl) => {
|
||||
const item = data.item;
|
||||
data.is_mod_or_admin = !!(session && (session.admin || session.is_moderator));
|
||||
data.can_manage_item = !!(session && (session.admin || session.is_moderator || session.user === item.username));
|
||||
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1);
|
||||
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1 && !(item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime])));
|
||||
data.user_has_favorited = !!(session && Array.isArray(item.favorites) && item.favorites.some(f => f.user === session.user));
|
||||
data.halls_slugs = Array.isArray(item.halls) ? item.halls.map(h => h.slug).join(',') : '';
|
||||
data.user_halls_slugs = Array.isArray(item.user_halls) ? item.user_halls.map(h => h.slug).join(',') : '';
|
||||
@@ -138,6 +138,7 @@ export default (router, tpl) => {
|
||||
data.item_rating_label = item.is_nsfl ? 'NSFL' : (item.is_nsfw ? 'NSFW' : (item.is_sfw ? 'SFW' : '?'));
|
||||
data.item_username_lower = (item.username || '').toLowerCase();
|
||||
data.is_flash_item = !!(item.mime && (item.mime.indexOf('flash') !== -1 || item.mime.indexOf('shockwave') !== -1));
|
||||
data.is_archive_item = !!(item.mime && item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime]));
|
||||
data.current_hall_slug = (data.tmp && data.tmp.hall && typeof data.tmp.hall === 'object') ? data.tmp.hall.slug : (data.tmp && data.tmp.hall ? data.tmp.hall : '');
|
||||
data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : '');
|
||||
data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : '';
|
||||
|
||||
@@ -348,7 +348,7 @@ export default (router, tpl) => {
|
||||
data.can_manage_item = !!(session && (session.admin || session.is_moderator || session.user === item.username));
|
||||
// Is the item's MIME type suitable for metadata extraction?
|
||||
// YouTube items use oEmbed via /meta/fetch; all non-flash MIME types are eligible.
|
||||
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1);
|
||||
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1 && !(item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime])));
|
||||
// Has the current user favorited this item?
|
||||
data.user_has_favorited = !!(session && Array.isArray(item.favorites) && item.favorites.some(f => f.user === session.user));
|
||||
// Hall columns for display
|
||||
@@ -359,6 +359,7 @@ export default (router, tpl) => {
|
||||
data.item_rating_label = item.is_nsfl ? 'NSFL' : (item.is_nsfw ? 'NSFW' : (item.is_sfw ? 'SFW' : '?'));
|
||||
data.item_username_lower = (item.username || '').toLowerCase();
|
||||
data.is_flash_item = !!(item.mime && (item.mime.indexOf('flash') !== -1 || item.mime.indexOf('shockwave') !== -1));
|
||||
data.is_archive_item = !!(item.mime && item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime]));
|
||||
data.current_hall_slug = (data.tmp && data.tmp.hall && typeof data.tmp.hall === 'object') ? data.tmp.hall.slug : (data.tmp && data.tmp.hall ? data.tmp.hall : '');
|
||||
data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : '');
|
||||
data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : '';
|
||||
|
||||
@@ -166,8 +166,12 @@ export default (router, tpl) => {
|
||||
const anchorId = qs.anchor ? parseInt(qs.anchor, 10) : null;
|
||||
|
||||
const swfMimes = ['application/x-shockwave-flash', 'application/vnd.adobe.flash.movie'];
|
||||
const archiveMimes = Object.entries(cfg.mimes)
|
||||
.filter(([mime, ext]) => mime.startsWith('application/') && !['swf', 'pdf'].includes(ext))
|
||||
.map(([mime]) => mime);
|
||||
const excludeSwfSQL = !cfg.websrv.enable_swf ? db`AND items.mime != ALL(${swfMimes})` : db``;
|
||||
const excludePdfSQL = db`AND items.mime != 'application/pdf'`;
|
||||
const excludeArchiveSQL = db`AND items.mime != ALL(${archiveMimes})`;
|
||||
const mimeParts = (mime || '').split(',').filter(m => ['video', 'audio', 'image'].includes(m));
|
||||
const mimeSQL = mimeParts.length > 0
|
||||
? db`AND (${mimeParts.map(m => db`items.mime ilike ${m + '/%'}`).reduce((a, b) => db`${a} OR ${b}`)})`
|
||||
@@ -246,7 +250,9 @@ export default (router, tpl) => {
|
||||
WHERE items.id = ${anchorId}
|
||||
AND items.active = true
|
||||
AND ${db.unsafe(modeQuery)}
|
||||
${excludeSwfSQL}
|
||||
${excludePdfSQL}
|
||||
${excludeArchiveSQL}
|
||||
${!req.session && nsfp ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND (${db.unsafe(nsfp)}))` : db``}
|
||||
`;
|
||||
// If the anchor item doesn't pass the rating filter, it's inaccessible to this user.
|
||||
@@ -264,6 +270,7 @@ export default (router, tpl) => {
|
||||
AND items.active = true
|
||||
${excludeSwfSQL}
|
||||
${excludePdfSQL}
|
||||
${excludeArchiveSQL}
|
||||
AND items.id != ${anchorId}
|
||||
${excludeSQL}
|
||||
${mimeSQL}
|
||||
@@ -287,6 +294,7 @@ export default (router, tpl) => {
|
||||
AND items.active = true
|
||||
${excludeSwfSQL}
|
||||
${excludePdfSQL}
|
||||
${excludeArchiveSQL}
|
||||
${cursorSQL}
|
||||
${excludeSQL}
|
||||
${mimeSQL}
|
||||
@@ -341,6 +349,7 @@ export default (router, tpl) => {
|
||||
fav_count: +row.fav_count || 0,
|
||||
comment_count: +row.comment_count || 0,
|
||||
is_swf: !!(row.mime === 'application/x-shockwave-flash' || row.mime === 'application/vnd.adobe.flash.movie'),
|
||||
is_archive: !!(row.mime && archiveMimes.includes(row.mime)),
|
||||
is_video: isVideo,
|
||||
is_youtube: isYouTube,
|
||||
is_audio: isAudio,
|
||||
|
||||
@@ -163,7 +163,7 @@ export default (router, tpl) => {
|
||||
const item = data.item;
|
||||
data.is_mod_or_admin = !!(session && (session.admin || session.is_moderator));
|
||||
data.can_manage_item = !!(session && (session.admin || session.is_moderator || session.user === item.username));
|
||||
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1);
|
||||
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1 && !(item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime])));
|
||||
data.user_has_favorited = !!(session && Array.isArray(item.favorites) && item.favorites.some(f => f.user === session.user));
|
||||
data.halls_slugs = Array.isArray(item.halls) ? item.halls.map(h => h.slug).join(',') : '';
|
||||
data.user_halls_slugs = Array.isArray(item.user_halls) ? item.user_halls.map(h => h.slug).join(',') : '';
|
||||
@@ -171,6 +171,7 @@ export default (router, tpl) => {
|
||||
data.item_rating_label = item.is_nsfl ? 'NSFL' : (item.is_nsfw ? 'NSFW' : (item.is_sfw ? 'SFW' : '?'));
|
||||
data.item_username_lower = (item.username || '').toLowerCase();
|
||||
data.is_flash_item = !!(item.mime && (item.mime.indexOf('flash') !== -1 || item.mime.indexOf('shockwave') !== -1));
|
||||
data.is_archive_item = !!(item.mime && item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime]));
|
||||
data.current_hall_slug = (data.tmp && data.tmp.hall && typeof data.tmp.hall === 'object') ? data.tmp.hall.slug : (data.tmp && data.tmp.hall ? data.tmp.hall : '');
|
||||
data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : '');
|
||||
data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : '';
|
||||
|
||||
@@ -77,6 +77,10 @@ const nginx502Fallback = `<html>
|
||||
</html>`;
|
||||
|
||||
// Login + Register modal injected before </body>
|
||||
// This string is built at startup so it can reference cfg.recaptcha values.
|
||||
const _rcEnabled = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key);
|
||||
const _rcSiteKey = (cfg.recaptcha && cfg.recaptcha.site_key) || '';
|
||||
|
||||
const gateLoginInjection = `
|
||||
<div id="hot-corner" style="position:fixed;bottom:0;left:0;width:20px;height:20px;z-index:9999;"></div>
|
||||
|
||||
@@ -117,6 +121,7 @@ const gateLoginInjection = `
|
||||
<input type="text" name="token" placeholder="Invite token" autocomplete="off"
|
||||
style="background:white;color:black;border:1px solid #bbb;padding:7px 10px;width:100%;box-sizing:border-box;font-size:14px;font-family:inherit;" />
|
||||
<input type="text" name="email_confirm_field" style="display:none !important;" tabindex="-1" autocomplete="off" />
|
||||
${_rcEnabled ? '<div id="gate-recaptcha" style="margin:4px 0;transform-origin:left top;"></div>' : ''}
|
||||
<button type="submit" id="gate-register-btn" style="background:#0051c3;color:white;border:none;padding:9px;font-weight:600;font-size:14px;cursor:pointer;font-family:inherit;"
|
||||
onmouseover="this.style.background='#003681'" onmouseout="if(!this.disabled)this.style.background='#0051c3'">Create account</button>
|
||||
<p style="text-align:center;font-size:0.85em;margin:6px 0 0;color:#555;">
|
||||
@@ -137,6 +142,7 @@ const gateLoginInjection = `
|
||||
function gateShowView(view) {
|
||||
document.getElementById('gate-login-view').style.display = view === 'login' ? '' : 'none';
|
||||
document.getElementById('gate-register-view').style.display = view === 'register' ? '' : 'none';
|
||||
if (view === 'register') gateRenderRecaptcha();
|
||||
}
|
||||
function gateSetError(id, msg) {
|
||||
var el = document.getElementById(id);
|
||||
@@ -152,6 +158,24 @@ const gateLoginInjection = `
|
||||
btn.style.cursor = loading ? 'default' : 'pointer';
|
||||
}
|
||||
|
||||
// reCAPTCHA
|
||||
var _gateRcWidgetId = null;
|
||||
var _gateRcLoaded = false;
|
||||
function gateRenderRecaptcha() {
|
||||
var el = document.getElementById('gate-recaptcha');
|
||||
if (!el || !window.grecaptcha) return;
|
||||
if (_gateRcWidgetId !== null) {
|
||||
try { grecaptcha.reset(_gateRcWidgetId); } catch(e) {}
|
||||
} else {
|
||||
_gateRcWidgetId = grecaptcha.render(el, { sitekey: '${_rcSiteKey}', theme: 'light' });
|
||||
}
|
||||
}
|
||||
window.onRecaptchaGateReady = function() {
|
||||
_gateRcLoaded = true;
|
||||
var rv = document.getElementById('gate-register-view');
|
||||
if (rv && rv.style.display !== 'none') gateRenderRecaptcha();
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var modal = document.getElementById('gate-modal');
|
||||
var close = document.getElementById('gate-modal-close');
|
||||
@@ -164,7 +188,7 @@ const gateLoginInjection = `
|
||||
var hc = document.getElementById('hot-corner');
|
||||
if (hc) hc.onclick = function() { openLoginGate('login'); };
|
||||
|
||||
// ── Login form ──────────────────────────────────────────────────────────
|
||||
// Login form
|
||||
document.getElementById('gate-login-form').onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
gateSetError('gate-login-error', '');
|
||||
@@ -182,23 +206,28 @@ const gateLoginInjection = `
|
||||
gateSetError('gate-login-error', d.msg || 'Login failed.');
|
||||
gateSetBtn('gate-login-btn', false);
|
||||
} else {
|
||||
// success — server set the cookie, reload to enter the site
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
// On redirect (301) fetch follows it — if the redirect lands on '/' we reload
|
||||
window.location.reload();
|
||||
});
|
||||
.catch(function() { window.location.reload(); });
|
||||
};
|
||||
|
||||
// ── Register form ───────────────────────────────────────────────────────
|
||||
// Register form
|
||||
document.getElementById('gate-register-form').onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
gateSetError('gate-register-error', '');
|
||||
gateSetError('gate-register-ok', '');
|
||||
gateSetBtn('gate-register-btn', true);
|
||||
var fd = new FormData(this);
|
||||
if (_gateRcWidgetId !== null && window.grecaptcha) {
|
||||
var token = grecaptcha.getResponse(_gateRcWidgetId);
|
||||
if (!token) {
|
||||
gateSetError('gate-register-error', 'Please complete the CAPTCHA.');
|
||||
gateSetBtn('gate-register-btn', false);
|
||||
return;
|
||||
}
|
||||
fd.append('g-recaptcha-response', token);
|
||||
}
|
||||
var body = new URLSearchParams(fd).toString();
|
||||
fetch('/register', {
|
||||
method: 'POST',
|
||||
@@ -210,6 +239,7 @@ const gateLoginInjection = `
|
||||
gateSetBtn('gate-register-btn', false);
|
||||
if (d.success === false) {
|
||||
gateSetError('gate-register-error', d.msg || 'Registration failed.');
|
||||
if (_gateRcWidgetId !== null && window.grecaptcha) { try { grecaptcha.reset(_gateRcWidgetId); } catch(e) {} }
|
||||
} else {
|
||||
document.getElementById('gate-register-ok').textContent = d.msg || 'Account created! You can now sign in.';
|
||||
document.getElementById('gate-register-ok').style.display = '';
|
||||
@@ -220,6 +250,7 @@ const gateLoginInjection = `
|
||||
.catch(function() {
|
||||
gateSetBtn('gate-register-btn', false);
|
||||
gateSetError('gate-register-error', 'An error occurred. Please try again.');
|
||||
if (_gateRcWidgetId !== null && window.grecaptcha) { try { grecaptcha.reset(_gateRcWidgetId); } catch(e) {} }
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -231,6 +262,7 @@ const gateLoginInjection = `
|
||||
if (_sb.length > 12) _sb = _sb.slice(-12);
|
||||
});
|
||||
</script>
|
||||
${_rcEnabled ? '<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaGateReady&render=explicit" async defer><\/script>' : ''}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1175,6 +1207,7 @@ process.on('uncaughtException', err => {
|
||||
enable_dynamic_thumbs: !!cfg.websrv.enable_dynamic_thumbs,
|
||||
comment_max_length: cfg.main.comment_max_length ?? null,
|
||||
enable_swf: !!cfg.websrv.enable_swf,
|
||||
enable_archive: !!cfg.websrv.enable_archive,
|
||||
enable_danmaku: cfg.websrv.enable_danmaku !== false,
|
||||
enable_item_title: cfg.websrv.enable_item_title !== false,
|
||||
enable_global_chat: !!cfg.websrv.enable_global_chat,
|
||||
|
||||
@@ -10,6 +10,15 @@ import { getManualApproval, getMinTags, getTrustedUploads, getBypassDuplicateChe
|
||||
import { parseMultipart, collectBody } from "./inc/multipart.mjs";
|
||||
import f0cklib from "./inc/routeinc/f0cklib.mjs";
|
||||
|
||||
// Derive archive MIME types from cfg.mimes — any application/* that isn't swf or pdf.
|
||||
// Adding a new archive type to config.json is sufficient; no code change needed.
|
||||
const ARCHIVE_MIMES = new Set(
|
||||
Object.entries(cfg.mimes)
|
||||
.filter(([mime, ext]) => mime.startsWith('application/') && !['swf', 'pdf'].includes(ext))
|
||||
.map(([mime]) => mime)
|
||||
);
|
||||
const isArchiveMime = (mime) => ARCHIVE_MIMES.has(mime);
|
||||
|
||||
// Helper for JSON response
|
||||
const sendJson = (res, data, code = 200) => {
|
||||
res.writeHead(code, { 'Content-Type': 'application/json' });
|
||||
@@ -264,6 +273,11 @@ export const handleUpload = async (req, res, self) => {
|
||||
return sendJson(res, { success: false, msg: 'PDF uploads are currently disabled.' }, 403);
|
||||
}
|
||||
|
||||
if (isArchiveMime(actualMime) && cfg.websrv.enable_archive === false) {
|
||||
await fs.unlink(tmpPath).catch(() => { });
|
||||
return sendJson(res, { success: false, msg: 'Archive uploads are currently disabled.' }, 403);
|
||||
}
|
||||
|
||||
// Reclassify audio-only MP4 containers (e.g. .m4a files detected as video/mp4)
|
||||
if (actualMime === 'video/mp4' || actualMime === 'video/quicktime') {
|
||||
const origExt = file.filename.split('.').pop().toLowerCase();
|
||||
@@ -308,8 +322,9 @@ export const handleUpload = async (req, res, self) => {
|
||||
}
|
||||
}
|
||||
|
||||
// PHash check
|
||||
// PHash check (skip for archives — binary blobs have no perceptual similarity)
|
||||
let phash = null;
|
||||
if (!isArchiveMime(actualMime)) {
|
||||
try {
|
||||
phash = await queue.generatePHash(tmpPath);
|
||||
if (phash && !getBypassDuplicateCheck()) {
|
||||
@@ -326,6 +341,7 @@ export const handleUpload = async (req, res, self) => {
|
||||
} catch (e) {
|
||||
console.error('[UPLOAD] PHash error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// When bypass is active, symlink to the existing file if one already exists on disk.
|
||||
// We write the symlink straight into cfg.paths.b so we can skip the pending flow entirely.
|
||||
@@ -375,9 +391,10 @@ export const handleUpload = async (req, res, self) => {
|
||||
// Suffix it so the INSERT can proceed — the file is genuinely a new item entry.
|
||||
const insertChecksum = getBypassDuplicateCheck() ? `${checksum}_bypass_${Date.now()}` : checksum;
|
||||
|
||||
// Probe pixel dimensions for images and videos (null for audio/flash/pdf/youtube)
|
||||
// Probe pixel dimensions for images and videos (null for audio/flash/pdf/archive/youtube)
|
||||
let itemWidth = null;
|
||||
let itemHeight = null;
|
||||
if (!isArchiveMime(actualMime)) {
|
||||
try {
|
||||
if (actualMime.startsWith('image/')) {
|
||||
// Use magick identify — handles all image formats, already present for thumbnailing
|
||||
@@ -412,6 +429,7 @@ export const handleUpload = async (req, res, self) => {
|
||||
} catch (dimErr) {
|
||||
console.warn(`[UPLOAD] Dimension probe failed for ${actualMime} (non-fatal):`, dimErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert
|
||||
const originalFilename = file.filename || null;
|
||||
@@ -575,6 +593,7 @@ export const handleUpload = async (req, res, self) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Action if auto-approved
|
||||
if (!manualApproval) {
|
||||
// Bust the count cache so page totals update immediately
|
||||
|
||||
@@ -30,6 +30,17 @@
|
||||
<div class="embed-responsive embed-responsive-16by9" style="background: #fff;border: 1px solid #444; overflow: hidden;">
|
||||
<iframe class="embed-responsive-item" src="{{ item.dest }}#toolbar=0" sandbox="allow-scripts allow-same-origin" loading="lazy" style="border: none; overscroll-behavior: contain !important;"></iframe>
|
||||
</div>
|
||||
@elseif(is_archive_item)
|
||||
<div class="embed-responsive embed-responsive-16by9 archive-download-view">
|
||||
<div class="archive-download-inner">
|
||||
<i class="fa-solid fa-file-zipper archive-download-icon"></i>
|
||||
<div class="archive-download-filename">{{ item.original_filename || item.dest.split('/').pop() }}</div>
|
||||
<div class="archive-download-size">{{ item.size }}</div>
|
||||
<a class="archive-download-btn" href="{{ item.dest }}" download>
|
||||
<i class="fa-solid fa-download"></i> Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<h1>404 - Not f0cked</h1>
|
||||
@endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@each(items as item)
|
||||
<a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
||||
<a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').replace('x-zip-compressed', 'zip').replace('x-rar-compressed', 'rar').replace('vnd.rar', 'rar').replace('x-7z-compressed', '7z').replace('x-tar', 'tar').replace('x-bzip2', 'bz2').replace('x-xz', 'xz').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
||||
<div class="thumb-indicators">
|
||||
@if(item.is_pinned)
|
||||
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<form id="upload-form" class="upload-form {{ shitpost_mode ? 'shitpost-mode-active' : '' }}" enctype="multipart/form-data" data-mimes='{!! mimes_json !!}' data-max-bytes="{{ max_file_size_bytes }}" data-min-tags="{{ min_tags }}" data-comment-max-length="{{ comment_max_length }}" data-enable-swf="{{ enable_swf ? '1' : '0' }}">
|
||||
<form id="upload-form" class="upload-form {{ shitpost_mode ? 'shitpost-mode-active' : '' }}" enctype="multipart/form-data" data-mimes='{!! mimes_json !!}' data-max-bytes="{{ max_file_size_bytes }}" data-min-tags="{{ min_tags }}" data-comment-max-length="{{ comment_max_length }}" data-enable-swf="{{ enable_swf ? '1' : '0' }}" data-enable-archive="{{ enable_archive ? '1' : '0' }}">
|
||||
<div class="form-section">
|
||||
@if(web_url_upload)
|
||||
<div class="upload-mode-tabs">
|
||||
|
||||
Reference in New Issue
Block a user