From 527a52ac4295a072927ff46761766a8b181a7603 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 12 Nov 2021 14:19:56 +0100 Subject: Add ability to filter out public videos from admin --- server/controllers/client.ts | 2 +- server/helpers/query.ts | 1 + server/middlewares/validators/videos/videos.ts | 13 ++++++++++--- .../video/sql/videos-id-list-query-builder.ts | 18 +++++++++++++----- server/models/video/video.ts | 15 +++++++++++++-- .../api/check-params/videos-common-filters.ts | 22 ++++++++++++++++++++-- server/tests/api/videos/videos-common-filters.ts | 8 +++++--- 7 files changed, 63 insertions(+), 16 deletions(-) (limited to 'server') diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 0a27ace76..703166c01 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts @@ -10,7 +10,7 @@ import { HttpStatusCode } from '@shared/models' import { root } from '../helpers/core-utils' import { STATIC_MAX_AGE } from '../initializers/constants' import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html' -import { asyncMiddleware, disableRobots, embedCSP } from '../middlewares' +import { asyncMiddleware, embedCSP } from '../middlewares' const clientsRouter = express.Router() diff --git a/server/helpers/query.ts b/server/helpers/query.ts index 97bbdfc65..1142d02e4 100644 --- a/server/helpers/query.ts +++ b/server/helpers/query.ts @@ -16,6 +16,7 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) { 'categoryOneOf', 'licenceOneOf', 'languageOneOf', + 'privacyOneOf', 'tagsOneOf', 'tagsAllOf', 'isLocal', diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 53643635c..4916decbf 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts @@ -7,6 +7,7 @@ import { isAbleToUploadVideo } from '@server/lib/user' import { getServerActor } from '@server/models/application/application' import { ExpressPromiseHandler } from '@server/types/express' import { MUserAccountId, MVideoFullLight } from '@server/types/models' +import { getAllPrivacies } from '@shared/core-utils' import { VideoInclude } from '@shared/models' import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared' import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' @@ -487,6 +488,10 @@ const commonVideosFiltersValidator = [ .optional() .customSanitizer(toArray) .custom(isStringArray).withMessage('Should have a valid one of language array'), + query('privacyOneOf') + .optional() + .customSanitizer(toArray) + .custom(isNumberArray).withMessage('Should have a valid one of privacy array'), query('tagsOneOf') .optional() .customSanitizer(toArray) @@ -536,10 +541,12 @@ const commonVideosFiltersValidator = [ // FIXME: deprecated in 4.0, to remove { if (req.query.filter === 'all-local') { - req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY + req.query.include = VideoInclude.NOT_PUBLISHED_STATE req.query.isLocal = true + req.query.privacyOneOf = getAllPrivacies() } else if (req.query.filter === 'all') { - req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY + req.query.include = VideoInclude.NOT_PUBLISHED_STATE + req.query.privacyOneOf = getAllPrivacies() } else if (req.query.filter === 'local') { req.query.isLocal = true } @@ -550,7 +557,7 @@ const commonVideosFiltersValidator = [ const user = res.locals.oauth?.token.User if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) { - if (req.query.include) { + if (req.query.include || req.query.privacyOneOf) { return res.fail({ status: HttpStatusCode.UNAUTHORIZED_401, message: 'You are not allowed to see all videos.' diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts index 4a882e790..d825225ab 100644 --- a/server/models/video/sql/videos-id-list-query-builder.ts +++ b/server/models/video/sql/videos-id-list-query-builder.ts @@ -40,6 +40,7 @@ export type BuildVideosListQueryOptions = { languageOneOf?: string[] tagsOneOf?: string[] tagsAllOf?: string[] + privacyOneOf?: VideoPrivacy[] uuids?: string[] @@ -138,11 +139,6 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { this.whereStateAvailable() } - // Only list videos with the appropriate priavcy - if (!(options.include & VideoInclude.HIDDEN_PRIVACY)) { - this.wherePrivacyAvailable(options.user) - } - if (options.videoPlaylistId) { this.joinPlaylist(options.videoPlaylistId) } @@ -187,6 +183,13 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { this.whereTagsAllOf(options.tagsAllOf) } + if (options.privacyOneOf) { + this.wherePrivacyOneOf(options.privacyOneOf) + } else { + // Only list videos with the appropriate priavcy + this.wherePrivacyAvailable(options.user) + } + if (options.uuids) { this.whereUUIDs(options.uuids) } @@ -435,6 +438,11 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { ) } + private wherePrivacyOneOf (privacyOneOf: VideoPrivacy[]) { + this.and.push('"video"."privacy" IN (:privacyOneOf)') + this.replacements.privacyOneOf = privacyOneOf + } + private whereUUIDs (uuids: string[]) { this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')') } diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 003741da0..69d009e04 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1041,6 +1041,7 @@ export class VideoModel extends Model>> { languageOneOf?: string[] tagsOneOf?: string[] tagsAllOf?: string[] + privacyOneOf?: VideoPrivacy[] accountId?: number videoChannelId?: number @@ -1059,6 +1060,7 @@ export class VideoModel extends Model>> { search?: string }) { VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) + VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user) const trendingDays = options.sort.endsWith('trending') ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS @@ -1082,6 +1084,7 @@ export class VideoModel extends Model>> { 'languageOneOf', 'tagsOneOf', 'tagsAllOf', + 'privacyOneOf', 'isLocal', 'include', 'displayOnlyForFollower', @@ -1119,6 +1122,7 @@ export class VideoModel extends Model>> { languageOneOf?: string[] tagsOneOf?: string[] tagsAllOf?: string[] + privacyOneOf?: VideoPrivacy[] displayOnlyForFollower: DisplayOnlyForFollowerOptions | null @@ -1140,6 +1144,7 @@ export class VideoModel extends Model>> { uuids?: string[] }) { VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) + VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user) const serverActor = await getServerActor() @@ -1153,6 +1158,7 @@ export class VideoModel extends Model>> { 'languageOneOf', 'tagsOneOf', 'tagsAllOf', + 'privacyOneOf', 'user', 'isLocal', 'host', @@ -1510,14 +1516,19 @@ export class VideoModel extends Model>> { private static throwIfPrivateIncludeWithoutUser (include: VideoInclude, user: MUserAccountId) { if (VideoModel.isPrivateInclude(include) && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) { - throw new Error('Try to filter all-local but no user has not the see all videos right') + throw new Error('Try to filter all-local but user cannot see all videos') + } + } + + private static throwIfPrivacyOneOfWithoutUser (privacyOneOf: VideoPrivacy[], user: MUserAccountId) { + if (privacyOneOf && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) { + throw new Error('Try to choose video privacies but user cannot see all videos') } } private static isPrivateInclude (include: VideoInclude) { return include & VideoInclude.BLACKLISTED || include & VideoInclude.BLOCKED_OWNER || - include & VideoInclude.HIDDEN_PRIVACY || include & VideoInclude.NOT_PUBLISHED_STATE } diff --git a/server/tests/api/check-params/videos-common-filters.ts b/server/tests/api/check-params/videos-common-filters.ts index afe42b0d5..f2b5bee8e 100644 --- a/server/tests/api/check-params/videos-common-filters.ts +++ b/server/tests/api/check-params/videos-common-filters.ts @@ -9,7 +9,7 @@ import { setAccessTokensToServers, setDefaultVideoChannel } from '@shared/extra-utils' -import { HttpStatusCode, UserRole, VideoInclude } from '@shared/models' +import { HttpStatusCode, UserRole, VideoInclude, VideoPrivacy } from '@shared/models' describe('Test video filters validators', function () { let server: PeerTubeServer @@ -112,7 +112,7 @@ describe('Test video filters validators', function () { const validIncludes = [ VideoInclude.NONE, - VideoInclude.HIDDEN_PRIVACY, + VideoInclude.BLOCKED_OWNER, VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED ] @@ -120,6 +120,7 @@ describe('Test video filters validators', function () { token?: string isLocal?: boolean include?: VideoInclude + privacyOneOf?: VideoPrivacy[] expectedStatus: HttpStatusCode }) { const paths = [ @@ -136,6 +137,7 @@ describe('Test video filters validators', function () { token: options.token || server.accessToken, query: { isLocal: options.isLocal, + privacyOneOf: options.privacyOneOf, include: options.include }, expectedStatus: options.expectedStatus @@ -143,6 +145,22 @@ describe('Test video filters validators', function () { } } + it('Should fail with a bad privacyOneOf', async function () { + await testEndpoints({ privacyOneOf: [ 'toto' ] as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with a good privacyOneOf', async function () { + await testEndpoints({ privacyOneOf: [ VideoPrivacy.INTERNAL ], expectedStatus: HttpStatusCode.OK_200 }) + }) + + it('Should fail to use privacyOneOf with a simple user', async function () { + await testEndpoints({ + privacyOneOf: [ VideoPrivacy.INTERNAL ], + token: userAccessToken, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + it('Should fail with a bad include', async function () { await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) }) diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts index 4f22d4ac3..ca5f42173 100644 --- a/server/tests/api/videos/videos-common-filters.ts +++ b/server/tests/api/videos/videos-common-filters.ts @@ -138,6 +138,7 @@ describe('Test videos filter', function () { hasWebtorrentFiles?: boolean hasHLSFiles?: boolean include?: VideoInclude + privacyOneOf?: VideoPrivacy[] category?: number tagsAllOf?: string[] token?: string @@ -148,7 +149,7 @@ describe('Test videos filter', function () { path: options.path, token: options.token ?? options.server.accessToken, query: { - ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles' ]), + ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles', 'privacyOneOf' ]), sort: 'createdAt' }, @@ -162,6 +163,7 @@ describe('Test videos filter', function () { server: PeerTubeServer isLocal?: boolean include?: VideoInclude + privacyOneOf?: VideoPrivacy[] token?: string expectedStatus?: HttpStatusCode }) { @@ -195,7 +197,7 @@ describe('Test videos filter', function () { server, token, isLocal: true, - include: VideoInclude.HIDDEN_PRIVACY + privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ] }) for (const names of namesResults) { @@ -216,7 +218,7 @@ describe('Test videos filter', function () { const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({ server, token, - include: VideoInclude.HIDDEN_PRIVACY + privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ] }) expect(channelVideos).to.have.lengthOf(3) -- cgit v1.2.3