gdfgd
This commit is contained in:
@@ -174,8 +174,8 @@ export async function getOrCreateAnonUser(pubkey, fingerprint, req = null, hwFin
|
||||
const userId = userRows[0].id;
|
||||
|
||||
await db`
|
||||
INSERT INTO user_options (user_id, mode, theme, fullscreen, avatar, display_name)
|
||||
VALUES (${userId}, 0, 'amoled', 0, null, 'Anonymous')
|
||||
INSERT INTO user_options (user_id, mode, theme, fullscreen, avatar, display_name, use_alternative_infobox)
|
||||
VALUES (${userId}, 0, 'amoled', 0, null, 'Anonymous', ${cfg.websrv.user_alternative_infobox !== false})
|
||||
ON CONFLICT (user_id) DO NOTHING
|
||||
`;
|
||||
|
||||
|
||||
@@ -18,16 +18,12 @@ const computeBaseMode = (mode, ratings, session) => {
|
||||
let effMode = Number(mode ?? 0);
|
||||
const ratingsArr = (Array.isArray(ratings) && ratings.length > 0) ? ratings : null;
|
||||
|
||||
// For guest sessions, sanitize ratingsArr to only allow permitted ratings
|
||||
// For guest sessions, strictly force SFW mode and SFW rating
|
||||
let safeRatingsArr = ratingsArr;
|
||||
if (!session && ratingsArr) {
|
||||
const allowedRatings = ['sfw'];
|
||||
if (cfg.websrv.public_nsfw) allowedRatings.push('nsfw');
|
||||
if (cfg.websrv.public_untagged) allowedRatings.push('untagged');
|
||||
safeRatingsArr = ratingsArr.filter(r => allowedRatings.includes(r));
|
||||
if (safeRatingsArr.length === 0) {
|
||||
return "1 = 0";
|
||||
}
|
||||
const isGuest = !session || !session.user;
|
||||
if (isGuest) {
|
||||
effMode = 0;
|
||||
safeRatingsArr = ['sfw'];
|
||||
} else if (isAnonSession(session)) {
|
||||
const allowedModes = getAnonAllowedModes();
|
||||
const canFilter = canAnonDo('filter');
|
||||
@@ -35,10 +31,14 @@ const computeBaseMode = (mode, ratings, session) => {
|
||||
if (!canFilter) {
|
||||
safeRatingsArr = null;
|
||||
effMode = 0;
|
||||
} else if (safeRatingsArr) {
|
||||
safeRatingsArr = safeRatingsArr.filter(r => allowedModes.includes(r));
|
||||
if (safeRatingsArr.length === 0) {
|
||||
return "1 = 0";
|
||||
} else {
|
||||
if (safeRatingsArr) {
|
||||
safeRatingsArr = safeRatingsArr.filter(r => allowedModes.includes(r));
|
||||
if (safeRatingsArr.length === 0) {
|
||||
safeRatingsArr = (allowedModes.length === 1 && !allowedModes.includes('all'))
|
||||
? [allowedModes[0]]
|
||||
: (allowedModes.length < 5 ? [...allowedModes] : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,11 +84,11 @@ const computeBaseMode = (mode, ratings, session) => {
|
||||
}
|
||||
} else if (effMode === 2) {
|
||||
if (!cfg.websrv.public_untagged) {
|
||||
baseMode = "1 = 0";
|
||||
baseMode = "items.id in (select item_id from tags_assign where tag_id = 1)";
|
||||
}
|
||||
} else if (effMode === 1) {
|
||||
if (!cfg.websrv.public_nsfw) {
|
||||
baseMode = "1 = 0";
|
||||
baseMode = "items.id in (select item_id from tags_assign where tag_id = 1)";
|
||||
}
|
||||
} else if (effMode === 4) {
|
||||
baseMode = "1 = 0";
|
||||
|
||||
@@ -34,10 +34,11 @@ export default (router, tpl) => {
|
||||
|
||||
if (cfg.main.development) console.log(`[${new Date().toISOString()}] [AJAX] Starting item load for ${req.params.itemid}`);
|
||||
|
||||
const isGuest = !req.session || !req.session.user;
|
||||
const isRandom = query.random === '1' || req.cookies.random_mode === '1';
|
||||
const reqMode = query.mode !== undefined ? +query.mode : req.mode;
|
||||
const reqMode = isGuest ? 0 : (query.mode !== undefined ? +query.mode : req.mode);
|
||||
const ratingsRaw = req.cookies.ratings;
|
||||
const ratingsArr = (reqMode === 2 || reqMode === 3) ? null : (ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null);
|
||||
const ratingsArr = isGuest ? ['sfw'] : ((reqMode === 2 || reqMode === 3) ? null : (ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null));
|
||||
|
||||
const itemid = req.params.itemid || req.url.pathname.match(/\/ajax\/item\/([a-zA-Z0-9_-]{11}|\d+)/)?.[1];
|
||||
const data = await f0cklib.getf0ck({
|
||||
@@ -256,10 +257,11 @@ export default (router, tpl) => {
|
||||
query = req.url.qs || {};
|
||||
}
|
||||
|
||||
const isGuest = !req.session || !req.session.user;
|
||||
const page = parseInt(query.page) || 1;
|
||||
const isRandom = query.random === '1' || req.cookies.random_mode === '1';
|
||||
const ratingsRaw = req.cookies.ratings;
|
||||
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
|
||||
const ratingsArr = isGuest ? ['sfw'] : (ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null);
|
||||
|
||||
const data = await f0cklib.getf0cks({
|
||||
page: page,
|
||||
@@ -269,7 +271,7 @@ export default (router, tpl) => {
|
||||
userHall: query.userHall || null,
|
||||
userHallOwner: query.userHallOwner || null,
|
||||
mime: query.mime || (req.cookies.mime || null),
|
||||
mode: query.mode !== undefined ? +query.mode : req.mode,
|
||||
mode: isGuest ? 0 : (query.mode !== undefined ? +query.mode : req.mode),
|
||||
ratings: ratingsArr,
|
||||
session: req.session,
|
||||
user_id: req.session?.id,
|
||||
|
||||
@@ -9,6 +9,259 @@ import { logAnonActivity } from "../../anon_auth.mjs";
|
||||
import { canAnonDo, isAnonSession } from "../../settings.mjs";
|
||||
|
||||
export default router => {
|
||||
router.post(/^\/api\/v2\/tags\/bulk\/?$/, lib.loggedin, async (req, res) => {
|
||||
const isModOrAdmin = !!(req.session?.admin || req.session?.is_moderator);
|
||||
if (!isModOrAdmin) {
|
||||
return res.json({ success: false, msg: 'Bulk selection is only available to administrators and moderators' }, 403);
|
||||
}
|
||||
|
||||
const action = req.body?.action || req.post?.action || 'add';
|
||||
const rawIds = req.body?.item_ids || req.post?.item_ids;
|
||||
if (!rawIds || !Array.isArray(rawIds) || rawIds.length === 0) {
|
||||
return res.json({ success: false, msg: 'No items selected' }, 400);
|
||||
}
|
||||
|
||||
// Sanitize item IDs
|
||||
const itemIds = [...new Set(rawIds.map(id => +id).filter(id => Number.isInteger(id) && id > 0))].slice(0, 500);
|
||||
if (itemIds.length === 0) {
|
||||
return res.json({ success: false, msg: 'Invalid item IDs' }, 400);
|
||||
}
|
||||
|
||||
// 1. ADD TAGS
|
||||
if (action === 'add') {
|
||||
if (isAnonSession(req.session) && !canAnonDo('tag')) {
|
||||
return res.json({ success: false, msg: 'Anonymous tagging is disabled' }, 403);
|
||||
}
|
||||
|
||||
let rawTags = req.body?.tags || req.post?.tags;
|
||||
if (typeof rawTags === 'string') {
|
||||
rawTags = rawTags.split(/[\n,]+/).map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
if (!Array.isArray(rawTags) || rawTags.length === 0) {
|
||||
return res.json({ success: false, msg: 'No tags provided' }, 400);
|
||||
}
|
||||
|
||||
const protectedTags = ['sfw', 'nsfw', 'nsfl'];
|
||||
const validTags = [...new Set(
|
||||
rawTags
|
||||
.map(t => (typeof t === 'string' ? t.trim() : ''))
|
||||
.filter(t => t.length > 0 && t.length <= 85 && !protectedTags.includes(t.toLowerCase()))
|
||||
)];
|
||||
|
||||
if (validTags.length === 0) {
|
||||
return res.json({ success: false, msg: 'Tags invalid or contain only reserved names' }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const tagIds = [];
|
||||
for (const tagname of validTags) {
|
||||
let tagRow = await db`
|
||||
SELECT id FROM "tags"
|
||||
WHERE normalized = slugify(${tagname})
|
||||
LIMIT 1
|
||||
`;
|
||||
let tagid = tagRow?.[0]?.id;
|
||||
if (!tagid) {
|
||||
const created = await db`
|
||||
INSERT INTO "tags" ${db({ tag: tagname })}
|
||||
RETURNING id
|
||||
`;
|
||||
tagid = created?.[0]?.id;
|
||||
}
|
||||
if (tagid) tagIds.push({ id: +tagid, name: tagname });
|
||||
}
|
||||
|
||||
// Insert assignments
|
||||
for (const postid of itemIds) {
|
||||
for (const { id: tagid } of tagIds) {
|
||||
try {
|
||||
await db`
|
||||
INSERT INTO "tags_assign" (tag_id, item_id, user_id)
|
||||
VALUES (${tagid}, ${postid}, ${+req.session.id})
|
||||
ON CONFLICT DO NOTHING
|
||||
`;
|
||||
} catch (assignErr) {
|
||||
// Ignore duplicate errors if table has constraint
|
||||
if (assignErr.code !== '23505') {
|
||||
console.warn(`[BULK_TAG_ASSIGN_WARN] Item ${postid}, tag ${tagid}:`, assignErr.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await audit.log(req.session.id, 'bulk_add_tags', 'items', null, { item_ids: itemIds, tags: validTags }).catch(() => {});
|
||||
if (req.session?.is_anon) {
|
||||
await logAnonActivity(req, {
|
||||
action: 'bulk_tag',
|
||||
targetId: itemIds[0],
|
||||
details: { item_ids: itemIds, tags: validTags }
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// Notify affected items asynchronously
|
||||
(async () => {
|
||||
for (const postid of itemIds) {
|
||||
try {
|
||||
const freshTags = await lib.getTags(postid);
|
||||
await db.notify('tags', JSON.stringify({ item_id: postid, fresh: true, tags: freshTags }));
|
||||
} catch {}
|
||||
}
|
||||
})().catch(() => {});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
count: itemIds.length,
|
||||
tags: validTags,
|
||||
msg: `Successfully added ${validTags.length} tag(s) to ${itemIds.length} item(s)`
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[BULK_ADD_TAGS_ERROR]', err);
|
||||
return res.json({ success: false, msg: 'Failed to add tags' }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. REMOVE TAGS
|
||||
if (action === 'remove') {
|
||||
const isModOrAdmin = !!(req.session.admin || req.session.is_moderator);
|
||||
|
||||
let rawTags = req.body?.tags || req.post?.tags;
|
||||
if (typeof rawTags === 'string') {
|
||||
rawTags = rawTags.split(/[\n,]+/).map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
if (!Array.isArray(rawTags) || rawTags.length === 0) {
|
||||
return res.json({ success: false, msg: 'No tags provided to remove' }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
// Resolve tags to remove
|
||||
const tagRows = await db`
|
||||
SELECT id, tag, normalized FROM "tags"
|
||||
WHERE normalized IN ${db(rawTags.map(t => t.trim().toLowerCase()))}
|
||||
`;
|
||||
if (tagRows.length === 0) {
|
||||
return res.json({ success: false, msg: 'Tags not found' }, 404);
|
||||
}
|
||||
const tagIds = tagRows.map(r => r.id);
|
||||
|
||||
if (isModOrAdmin) {
|
||||
await db`
|
||||
DELETE FROM "tags_assign"
|
||||
WHERE item_id IN ${db(itemIds)}
|
||||
AND tag_id IN ${db(tagIds)}
|
||||
`;
|
||||
} else {
|
||||
// Normal user can only remove tags from items they own
|
||||
await db`
|
||||
DELETE FROM "tags_assign"
|
||||
WHERE item_id IN ${db(itemIds)}
|
||||
AND tag_id IN ${db(tagIds)}
|
||||
AND item_id IN (
|
||||
SELECT id FROM items WHERE username = ${req.session.user}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
await audit.log(req.session.id, 'bulk_remove_tags', 'items', null, { item_ids: itemIds, tags: tagRows.map(r => r.tag) }).catch(() => {});
|
||||
|
||||
// Realtime notifications
|
||||
(async () => {
|
||||
for (const postid of itemIds) {
|
||||
try {
|
||||
const freshTags = await lib.getTags(postid);
|
||||
await db.notify('tags', JSON.stringify({ item_id: postid, fresh: true, tags: freshTags }));
|
||||
} catch {}
|
||||
}
|
||||
})().catch(() => {});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
count: itemIds.length,
|
||||
msg: `Successfully removed tag(s) from ${itemIds.length} item(s)`
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[BULK_REMOVE_TAGS_ERROR]', err);
|
||||
return res.json({ success: false, msg: 'Failed to remove tags' }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. SET RATING (sfw / nsfw / nsfl)
|
||||
if (action === 'set_rating') {
|
||||
if (isAnonSession(req.session) && !canAnonDo('rate_item')) {
|
||||
return res.json({ success: false, msg: 'Anonymous rating is disabled' }, 403);
|
||||
}
|
||||
|
||||
const rating = (req.body?.rating || req.post?.rating || '').toLowerCase();
|
||||
if (!['sfw', 'nsfw', 'nsfl'].includes(rating)) {
|
||||
return res.json({ success: false, msg: 'Invalid rating. Choose sfw, nsfw, or nsfl' }, 400);
|
||||
}
|
||||
|
||||
const isModOrAdmin = !!(req.session.admin || req.session.is_moderator);
|
||||
const nsflTagRow = await db`SELECT id FROM tags WHERE normalized = 'nsfl' LIMIT 1`;
|
||||
const nsflId = nsflTagRow.length > 0 ? nsflTagRow[0].id : (cfg.nsfl_tag_id || 11517);
|
||||
const targetTagId = rating === 'sfw' ? 1 : (rating === 'nsfw' ? 2 : nsflId);
|
||||
|
||||
try {
|
||||
let eligibleIds = itemIds;
|
||||
if (!isModOrAdmin) {
|
||||
const ownedItems = await db`
|
||||
SELECT id FROM items
|
||||
WHERE id IN ${db(itemIds)}
|
||||
AND username = ${req.session.user}
|
||||
AND active = true AND is_deleted = false
|
||||
`;
|
||||
eligibleIds = ownedItems.map(r => r.id);
|
||||
}
|
||||
|
||||
if (eligibleIds.length === 0) {
|
||||
return res.json({ success: false, msg: 'You do not have permission to rate the selected items' }, 403);
|
||||
}
|
||||
|
||||
await db.begin(async sql => {
|
||||
// Delete existing rating tags
|
||||
await sql`
|
||||
DELETE FROM tags_assign
|
||||
WHERE item_id IN ${sql(eligibleIds)}
|
||||
AND (tag_id IN (1, 2, ${nsflId}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||
`;
|
||||
|
||||
// Insert new rating tags
|
||||
for (const id of eligibleIds) {
|
||||
await sql`
|
||||
INSERT INTO tags_assign (item_id, tag_id, user_id)
|
||||
VALUES (${id}, ${targetTagId}, ${+req.session.id})
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
await audit.log(req.session.id, 'bulk_set_rating', 'items', null, { item_ids: eligibleIds, rating }).catch(() => {});
|
||||
|
||||
// Queue blur thumbnail verification where needed
|
||||
(async () => {
|
||||
for (const id of eligibleIds) {
|
||||
try {
|
||||
const blurPath = path.join(cfg.paths.t, `${id}_blur.webp`);
|
||||
await fs.promises.access(blurPath).catch(() => queue.genBlurredThumbnail(id, false));
|
||||
const freshTags = await lib.getTags(id);
|
||||
await db.notify('tags', JSON.stringify({ item_id: id, fresh: true, tags: freshTags }));
|
||||
} catch {}
|
||||
}
|
||||
})().catch(() => {});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
count: eligibleIds.length,
|
||||
rating,
|
||||
msg: `Successfully set rating to ${rating.toUpperCase()} for ${eligibleIds.length} item(s)`
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[BULK_SET_RATING_ERROR]', err);
|
||||
return res.json({ success: false, msg: 'Failed to update rating' }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({ success: false, msg: 'Unknown bulk action' }, 400);
|
||||
});
|
||||
|
||||
router.group(/^\/api\/v2\/tags\/(?<postid>\d+)/, group => {
|
||||
group.get(/$/, lib.loggedin, async (req, res) => {
|
||||
// get tags
|
||||
|
||||
@@ -645,7 +645,19 @@ export default (router, tpl) => {
|
||||
const modeMatch = req.url.pathname.match(/^\/mode\/(\d)/);
|
||||
const mode = modeMatch ? +modeMatch[1] : 0;
|
||||
|
||||
if (isAnonSession(req.session)) {
|
||||
const isGuest = !req.session || !req.session.user;
|
||||
if (isGuest) {
|
||||
if (mode !== 0) {
|
||||
if (req.headers['x-requested-with'] === 'XMLHttpRequest') {
|
||||
return res.reply({
|
||||
code: 403,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ success: false, msg: 'Only SFW mode is allowed for guests' })
|
||||
});
|
||||
}
|
||||
return res.redirect('/');
|
||||
}
|
||||
} else if (isAnonSession(req.session)) {
|
||||
if (!canAnonDo('filter') || !canAnonMode(mode)) {
|
||||
if (req.headers['x-requested-with'] === 'XMLHttpRequest') {
|
||||
return res.reply({
|
||||
|
||||
Reference in New Issue
Block a user