diff --git a/src/routes/getLockCategories.ts b/src/routes/getLockCategories.ts index c4992e7..4362035 100644 --- a/src/routes/getLockCategories.ts +++ b/src/routes/getLockCategories.ts @@ -3,17 +3,12 @@ import { Logger } from "../utils/logger"; import { Request, Response } from "express"; import { ActionType, Category, VideoID } from "../types/segments.model"; import { getService } from "../utils/getService"; +import { parseActionTypes } from "../utils/parseParams"; export async function getLockCategories(req: Request, res: Response): Promise { const videoID = req.query.videoID as VideoID; const service = getService(req.query.service as string); - const actionTypes: ActionType[] = req.query.actionTypes - ? JSON.parse(req.query.actionTypes as string) - : req.query.actionType - ? Array.isArray(req.query.actionType) - ? req.query.actionType - : [req.query.actionType] - : [ActionType.Skip, ActionType.Mute]; + const actionTypes: ActionType[] = parseActionTypes(req, [ActionType.Skip, ActionType.Mute]); if (!videoID || !Array.isArray(actionTypes)) { //invalid request return res.sendStatus(400); diff --git a/src/routes/getLockCategoriesByHash.ts b/src/routes/getLockCategoriesByHash.ts index 1aee441..80fdc9b 100644 --- a/src/routes/getLockCategoriesByHash.ts +++ b/src/routes/getLockCategoriesByHash.ts @@ -3,6 +3,7 @@ import { Logger } from "../utils/logger"; import { Request, Response } from "express"; import { hashPrefixTester } from "../utils/hashPrefixTester"; import { ActionType, Category, VideoID, VideoIDHash } from "../types/segments.model"; +import { parseActionTypes } from "../utils/parseParams"; interface LockResultByHash { videoID: VideoID, @@ -44,25 +45,13 @@ const mergeLocks = (source: DBLock[], actionTypes: ActionType[]): LockResultByHa export async function getLockCategoriesByHash(req: Request, res: Response): Promise { let hashPrefix = req.params.prefix as VideoIDHash; - let actionTypes: ActionType[] = []; - try { - actionTypes = req.query.actionTypes - ? JSON.parse(req.query.actionTypes as string) - : req.query.actionType - ? Array.isArray(req.query.actionType) - ? req.query.actionType - : [req.query.actionType] - : [ActionType.Skip, ActionType.Mute]; - if (!Array.isArray(actionTypes)) { - //invalid request - return res.sendStatus(400); - } - } catch (err) { + const actionTypes: ActionType[] = parseActionTypes(req, [ActionType.Skip, ActionType.Mute]); + if (!Array.isArray(actionTypes)) { //invalid request - return res.status(400).send("Invalid request: JSON parse error (actionTypes)"); + return res.sendStatus(400); } - if (!hashPrefixTester(req.params.prefix)) { + if (!hashPrefixTester(req.params.prefix)) { return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix } hashPrefix = hashPrefix.toLowerCase() as VideoIDHash; diff --git a/src/routes/getLockReason.ts b/src/routes/getLockReason.ts index 59ab528..0f0dc11 100644 --- a/src/routes/getLockReason.ts +++ b/src/routes/getLockReason.ts @@ -2,9 +2,8 @@ import { db } from "../databases/databases"; import { Logger } from "../utils/logger"; import { Request, Response } from "express"; import { Category, VideoID, ActionType } from "../types/segments.model"; -import { config } from "../config"; +import { filterInvalidCategoryActionType, parseActionTypes, parseCategories } from "../utils/parseParams"; -const categorySupportList = config.categorySupport; interface lockArray { category: Category; locked: number, @@ -13,62 +12,19 @@ interface lockArray { userName: string, } -const filterActionType = (actionTypes: ActionType[]) => { - const filterCategories = new Set(); - for (const [key, value] of Object.entries(categorySupportList)) { - for (const type of actionTypes) { - if (value.includes(type)) { - filterCategories.add(key as Category); - } - } - } - return [...filterCategories]; -}; - export async function getLockReason(req: Request, res: Response): Promise { const videoID = req.query.videoID as VideoID; - if (!videoID) { - // invalid request - return res.status(400).send("No videoID provided"); - } - let categories: Category[] = []; - let actionTypes: ActionType[] = []; - try { - actionTypes = req.query.actionTypes - ? JSON.parse(req.query.actionTypes as string) - : req.query.actionType - ? Array.isArray(req.query.actionType) - ? req.query.actionType - : [req.query.actionType] - : [ActionType.Skip, ActionType.Mute]; - if (!Array.isArray(actionTypes)) { - //invalid request - return res.status(400).send("actionTypes parameter does not match format requirements"); - } - } catch (error) { - return res.status(400).send("Bad parameter: actionTypes (invalid JSON)"); - } - const possibleCategories = filterActionType(actionTypes); + const actionTypes = parseActionTypes(req, [ActionType.Skip, ActionType.Mute]); + const categories = parseCategories(req, []); - try { - categories = req.query.categories - ? JSON.parse(req.query.categories as string) - : req.query.category - ? Array.isArray(req.query.category) - ? req.query.category - : [req.query.category] - : []; // default to empty, will be set to all - if (!Array.isArray(categories)) { - return res.status(400).send("Categories parameter does not match format requirements."); - } - } catch(error) { - return res.status(400).send("Bad parameter: categories (invalid JSON)"); - } + // invalid requests + const errors = []; + if (!videoID) errors.push("No videoID provided"); + if (!Array.isArray(actionTypes)) errors.push("actionTypes parameter does not match format requirements"); + if (!Array.isArray(categories)) errors.push("Categories parameter does not match format requirements."); + if (errors.length) return res.status(400).send(errors.join(", ")); // only take valid categories - const searchCategories = (categories.length === 0 ) - ? possibleCategories - : categories.filter(x => - possibleCategories.includes(x)); + const searchCategories = filterInvalidCategoryActionType(categories, actionTypes); try { // Get existing lock categories markers diff --git a/src/routes/getSearchSegments.ts b/src/routes/getSearchSegments.ts index 31b2c93..4fd2d88 100644 --- a/src/routes/getSearchSegments.ts +++ b/src/routes/getSearchSegments.ts @@ -2,6 +2,8 @@ import { Request, Response } from "express"; import { db } from "../databases/databases"; import { ActionType, Category, DBSegment, Service, VideoID, SortableFields } from "../types/segments.model"; import { getService } from "../utils/getService"; +import { parseActionTypes, parseCategories } from "../utils/parseParams"; + const maxSegmentsPerPage = 100; const defaultSegmentsPerPage = 10; @@ -73,25 +75,15 @@ async function handleGetSegments(req: Request, res: Response): Promise { } } catch (err) { /* istanbul ignore next */ + console.log(err) if (err instanceof SyntaxError) { return res.status(400).send("Invalid array in parameters"); } else return res.sendStatus(500); diff --git a/src/routes/shadowBanUser.ts b/src/routes/shadowBanUser.ts index cab5077..e0d0f40 100644 --- a/src/routes/shadowBanUser.ts +++ b/src/routes/shadowBanUser.ts @@ -6,11 +6,13 @@ import { Category, Service, VideoID, VideoIDHash } from "../types/segments.model import { UserID } from "../types/user.model"; import { QueryCacher } from "../utils/queryCacher"; import { isUserVIP } from "../utils/isUserVIP"; +import { parseCategories } from "../utils/parseParams"; export async function shadowBanUser(req: Request, res: Response): Promise { const userID = req.query.userID as UserID; const hashedIP = req.query.hashedIP as string; const adminUserIDInput = req.query.adminUserID as UserID; + const type = req.query.type as string ?? "1"; const enabled = req.query.enabled === undefined ? true @@ -19,10 +21,9 @@ export async function shadowBanUser(req: Request, res: Response): Promise typeof category === "string" && !(/[^a-z|_|-]/.test(category))); + const categories: Category[] = parseCategories(req, config.categoryList as Category[]); - if (adminUserIDInput == undefined || (userID == undefined && hashedIP == undefined)) { + if (adminUserIDInput == undefined || (userID == undefined && hashedIP == undefined || type !== "1" && type !== "2")) { //invalid request return res.sendStatus(400); } @@ -48,7 +49,7 @@ export async function shadowBanUser(req: Request, res: Response): Promise 0) { //remove them from the shadow ban list @@ -66,7 +67,7 @@ export async function shadowBanUser(req: Request, res: Response): Promise { // collect list for unshadowbanning - (await db.prepare("all", `SELECT "videoID", "hashedVideoID", "service", "votes", "views", "userID" FROM "sponsorTimes" WHERE "UUID" = ? AND "shadowHidden" = 1 AND "category" in (${categories.map((c) => `'${c}'`).join(",")})`, [UUID])) + (await db.prepare("all", `SELECT "videoID", "hashedVideoID", "service", "votes", "views", "userID" FROM "sponsorTimes" WHERE "UUID" = ? AND "shadowHidden" >= 1 AND "category" in (${categories.map((c) => `'${c}'`).join(",")})`, [UUID])) .forEach((videoInfo: {category: Category, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID}) => { QueryCacher.clearSegmentCache(videoInfo); } @@ -79,7 +80,7 @@ export async function shadowBanUser(req: Request, res: Response): Promise 0) { // apply unHideOldSubmissions if applicable if (unHideOldSubmissions) { - await unHideSubmissions(categories, userID); + await unHideSubmissions(categories, userID, type); return res.sendStatus(200); } @@ -100,7 +101,7 @@ export async function shadowBanUser(req: Request, res: Response): Promise `'${c}'`).join(",")}) +async function unHideSubmissions(categories: string[], userID: UserID, type = "1") { + await db.prepare("run", `UPDATE "sponsorTimes" SET "shadowHidden" = ${type} WHERE "userID" = ? AND "category" in (${categories.map((c) => `'${c}'`).join(",")}) AND NOT EXISTS ( SELECT "videoID", "category" FROM "lockCategories" WHERE "sponsorTimes"."videoID" = "lockCategories"."videoID" AND "sponsorTimes"."service" = "lockCategories"."service" AND "sponsorTimes"."category" = "lockCategories"."category")`, [userID]); diff --git a/src/utils/parseParams.ts b/src/utils/parseParams.ts new file mode 100644 index 0000000..258b78a --- /dev/null +++ b/src/utils/parseParams.ts @@ -0,0 +1,75 @@ +import { Request } from "express"; +import { ActionType, SegmentUUID, Category } from "../types/segments.model"; +import { config } from "../config"; + +type fn = (req: Request, fallback: any) => any[]; + +const syntaxErrorWrapper = (fn: fn, req: Request, fallback: any) => { + try { return fn(req, fallback); } + catch (e) { + return undefined; + } +}; + +const getCategories = (req: Request, fallback: Category[] ): string[] | Category[] => + req.query.categories + ? JSON.parse(req.query.categories as string) + : req.query.category + ? Array.isArray(req.query.category) + ? req.query.category + : [req.query.category] + : fallback; + +const validateString = (array: any[]): any[] => { + if (!Array.isArray(array)) return undefined; + return array + .filter((item: any) => typeof item === "string") + .filter((item: string) => !(/[^a-z|_|-]/.test(item))); +}; + +const filterActionType = (actionTypes: ActionType[]) => { + const filterCategories = new Set(); + for (const [key, value] of Object.entries(config.categorySupport)) { + for (const type of actionTypes) { + if (value.includes(type)) { + filterCategories.add(key as Category); + } + } + } + return [...filterCategories]; +}; + +export const filterInvalidCategoryActionType = (categories: Category[], actionTypes: ActionType[]): Category[] => + categories.filter((category: Category) => filterActionType(actionTypes).includes(category)); + +const getActionTypes = (req: Request, fallback: ActionType[]): ActionType[] => + req.query.actionTypes + ? JSON.parse(req.query.actionTypes as string) + : req.query.actionType + ? Array.isArray(req.query.actionType) + ? req.query.actionType + : [req.query.actionType] + : fallback; + +// fallback to empty array +const getRequiredSegments = (req: Request): SegmentUUID[] => + req.query.requiredSegments + ? JSON.parse(req.query.requiredSegments as string) + : req.query.requiredSegment + ? Array.isArray(req.query.requiredSegment) + ? req.query.requiredSegment + : [req.query.requiredSegment] + : []; + +export const parseCategories = (req: Request, fallback: Category[]): Category[] => { + const categories = syntaxErrorWrapper(getCategories, req, fallback); + return categories ? validateString(categories) : undefined; +}; + +export const parseActionTypes = (req: Request, fallback: ActionType[]): ActionType[] => { + const actionTypes = syntaxErrorWrapper(getActionTypes, req, fallback); + return actionTypes ? validateString(actionTypes) : undefined; +}; + +export const parseRequiredSegments = (req: Request): SegmentUUID[] | undefined => + syntaxErrorWrapper(getRequiredSegments, req, []); // never fall back diff --git a/src/utils/parseSkipSegments.ts b/src/utils/parseSkipSegments.ts index f9e5b96..1e0378c 100644 --- a/src/utils/parseSkipSegments.ts +++ b/src/utils/parseSkipSegments.ts @@ -2,42 +2,7 @@ import { Request } from "express"; import { ActionType, SegmentUUID, Category, Service } from "../types/segments.model"; import { getService } from "./getService"; -type fn = (req: Request) => any[]; - -const syntaxErrorWrapper = (fn: fn, req: Request) => { - try { return fn(req); } - catch (e) { return undefined; } -}; - -// Default to sponsor -const getCategories = (req: Request): Category[] => - req.query.categories - ? JSON.parse(req.query.categories as string) - : req.query.category - ? Array.isArray(req.query.category) - ? req.query.category - : [req.query.category] - : ["sponsor"]; - -// Default to skip -const getActionTypes = (req: Request): ActionType[] => - req.query.actionTypes - ? JSON.parse(req.query.actionTypes as string) - : req.query.actionType - ? Array.isArray(req.query.actionType) - ? req.query.actionType - : [req.query.actionType] - : [ActionType.Skip]; - -// Default to empty array -const getRequiredSegments = (req: Request): SegmentUUID[] => - req.query.requiredSegments - ? JSON.parse(req.query.requiredSegments as string) - : req.query.requiredSegment - ? Array.isArray(req.query.requiredSegment) - ? req.query.requiredSegment - : [req.query.requiredSegment] - : []; +import { parseCategories, parseActionTypes, parseRequiredSegments } from "./parseParams"; const errorMessage = (parameter: string) => `${parameter} parameter does not match format requirements.`; @@ -48,20 +13,14 @@ export function parseSkipSegments(req: Request): { service: Service; errors: string[]; } { - let categories: Category[] = syntaxErrorWrapper(getCategories, req); - const actionTypes: ActionType[] = syntaxErrorWrapper(getActionTypes, req); - const requiredSegments: SegmentUUID[] = syntaxErrorWrapper(getRequiredSegments, req); + const categories: Category[] = parseCategories(req, [ "sponsor" as Category ]); + const actionTypes: ActionType[] = parseActionTypes(req, [ActionType.Skip]); + const requiredSegments: SegmentUUID[] = parseRequiredSegments(req); const service: Service = getService(req.query.service, req.body.services); const errors: string[] = []; if (!Array.isArray(categories)) errors.push(errorMessage("categories")); - else { - // check category names for invalid characters - // and none string elements - categories = categories - .filter((item: any) => typeof item === "string") - .filter((category) => !(/[^a-z|_|-]/.test(category))); - if (categories.length === 0) errors.push("No valid categories provided."); - } + else if (categories.length === 0) errors.push("No valid categories provided."); + if (!Array.isArray(actionTypes)) errors.push(errorMessage("actionTypes")); if (!Array.isArray(requiredSegments)) errors.push(errorMessage("requiredSegments")); // finished parsing diff --git a/test/cases/shadowBanUser.ts b/test/cases/shadowBanUser.ts index a0b321c..dbe5297 100644 --- a/test/cases/shadowBanUser.ts +++ b/test/cases/shadowBanUser.ts @@ -11,22 +11,26 @@ describe("shadowBanUser", () => { const endpoint = "/api/shadowBanUser"; const VIPuserID = "shadow-ban-vip"; + const video = "shadowBanVideo"; + const videohash = getHash(video, 1); before(async () => { - const insertQuery = `INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "service", "videoDuration", "hidden", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; - await db.prepare("run", insertQuery, ["testtesttest", 1, 11, 2, 0, "shadow-1-uuid-0", "shadowBanned", 0, 50, "sponsor", "YouTube", 100, 0, 0, getHash("testtesttest", 1)]); - await db.prepare("run", insertQuery, ["testtesttest2", 1, 11, 2, 0, "shadow-1-uuid-0-1", "shadowBanned", 0, 50, "sponsor", "PeerTube", 120, 0, 0, getHash("testtesttest2", 1)]); - await db.prepare("run", insertQuery, ["testtesttest", 20, 33, 2, 0, "shadow-1-uuid-2", "shadowBanned", 0, 50, "intro", "YouTube", 101, 0, 0, getHash("testtesttest", 1)]); + const insertQuery = `INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "service", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; + await db.prepare("run", insertQuery, [video, 1, 11, 2, 0, "shadow-10", "shadowBanned", 0, 50, "sponsor", "YouTube", 0, videohash]); + await db.prepare("run", insertQuery, [video, 1, 11, 2, 0, "shadow-11", "shadowBanned", 0, 50, "sponsor", "PeerTube", 0, videohash]); + await db.prepare("run", insertQuery, [video, 20, 33, 2, 0, "shadow-12", "shadowBanned", 0, 50, "intro", "YouTube", 0, videohash]); - await db.prepare("run", insertQuery, ["testtesttest", 1, 11, 2, 0, "shadow-2-uuid-0", "shadowBanned2", 0, 50, "sponsor", "YouTube", 100, 0, 0, getHash("testtesttest", 1)]); - await db.prepare("run", insertQuery, ["testtesttest2", 1, 11, 2, 0, "shadow-2-uuid-0-1", "shadowBanned2", 0, 50, "sponsor", "PeerTube", 120, 0, 0, getHash("testtesttest2", 1)]); - await db.prepare("run", insertQuery, ["testtesttest", 20, 33, 2, 0, "shadow-2-uuid-2", "shadowBanned2", 0, 50, "intro", "YouTube", 101, 0, 0, getHash("testtesttest", 1)]); + await db.prepare("run", insertQuery, [video, 1, 11, 2, 0, "shadow-20", "shadowBanned2", 0, 50, "sponsor", "YouTube", 0, videohash]); + await db.prepare("run", insertQuery, [video, 1, 11, 2, 0, "shadow-21", "shadowBanned2", 0, 50, "sponsor", "PeerTube", 0, videohash]); + await db.prepare("run", insertQuery, [video, 20, 33, 2, 0, "shadow-22", "shadowBanned2", 0, 50, "intro", "YouTube", 0, videohash]); - await db.prepare("run", insertQuery, ["testtesttest", 1, 11, 2, 0, "shadow-3-uuid-0", "shadowBanned3", 0, 50, "sponsor", "YouTube", 100, 0, 1, getHash("testtesttest", 1)]); - await db.prepare("run", insertQuery, ["testtesttest2", 1, 11, 2, 0, "shadow-3-uuid-0-1", "shadowBanned3", 0, 50, "sponsor", "PeerTube", 120, 0, 1, getHash("testtesttest2", 1)]); - await db.prepare("run", insertQuery, ["testtesttest", 20, 33, 2, 0, "shadow-3-uuid-2", "shadowBanned3", 0, 50, "intro", "YouTube", 101, 0, 1, getHash("testtesttest", 1)]); + await db.prepare("run", insertQuery, [video, 1, 11, 2, 0, "shadow-30", "shadowBanned3", 0, 50, "sponsor", "YouTube", 1, videohash]); + await db.prepare("run", insertQuery, [video, 1, 11, 2, 0, "shadow-31", "shadowBanned3", 0, 50, "sponsor", "PeerTube", 1, videohash]); + await db.prepare("run", insertQuery, [video, 20, 33, 2, 0, "shadow-32", "shadowBanned3", 0, 50, "intro", "YouTube", 1, videohash]); - await db.prepare("run", insertQuery, ["testtesttest", 21, 34, 2, 0, "shadow-4-uuid-1", "shadowBanned4", 0, 50, "sponsor", "YouTube", 101, 0, 0, getHash("testtesttest", 1)]); + await db.prepare("run", insertQuery, [video, 21, 34, 2, 0, "shadow-40", "shadowBanned4", 0, 50, "sponsor", "YouTube", 0, videohash]); + + await db.prepare("run", insertQuery, [video, 20, 10, 2, 0, "shadow-50", "shadowBanned5", 0, 50, "sponsor", "YouTube", 0, videohash]); await db.prepare("run", `INSERT INTO "shadowBannedUsers" ("userID") VALUES(?)`, ["shadowBanned3"]); await db.prepare("run", `INSERT INTO "shadowBannedUsers" ("userID") VALUES(?)`, ["shadowBanned4"]); @@ -220,4 +224,54 @@ describe("shadowBanUser", () => { }) .catch(err => done(err)); }); + + it("Should be able to shadowban user with different type", (done) => { + const userID = "shadowBanned5"; + client({ + method: "POST", + url: endpoint, + params: { + userID, + adminUserID: VIPuserID, + enabled: true, + categories: `["sponsor"]`, + unHideOldSubmissions: true, + type: "2" + } + }) + .then(async res => { + assert.strictEqual(res.status, 200); + const type2Videos = await getShadowBanSegmentCategory(userID, 2); + const type1Videos = await getShadowBanSegmentCategory(userID, 1); + const type0Videos = await getShadowBanSegmentCategory(userID, 0); + const shadowRow = await getShadowBan(userID); + assert.ok(shadowRow); // ban still exists + assert.ok(type2Videos.length > 0); // videos at type 2 + assert.strictEqual(type1Videos.length, 0); // no videos at type 1 + assert.strictEqual(type0Videos.length, 0); // no videos at type 0 + done(); + }) + .catch(err => done(err)); + }); + + it("Should not be able to shadowban user with invalid type", (done) => { + const userID = "shadowBanned5"; + client({ + method: "POST", + url: endpoint, + params: { + userID, + adminUserID: VIPuserID, + enabled: true, + categories: `["sponsor"]`, + unHideOldSubmissions: true, + type: "3" + } + }) + .then(res => { + assert.strictEqual(res.status, 400); + done(); + }) + .catch(err => done(err)); + }); });