aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/client.ts2
-rw-r--r--server/helpers/query.ts1
-rw-r--r--server/middlewares/validators/videos/videos.ts13
-rw-r--r--server/models/video/sql/videos-id-list-query-builder.ts18
-rw-r--r--server/models/video/video.ts15
-rw-r--r--server/tests/api/check-params/videos-common-filters.ts22
-rw-r--r--server/tests/api/videos/videos-common-filters.ts8
7 files changed, 63 insertions, 16 deletions
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'
10import { root } from '../helpers/core-utils' 10import { root } from '../helpers/core-utils'
11import { STATIC_MAX_AGE } from '../initializers/constants' 11import { STATIC_MAX_AGE } from '../initializers/constants'
12import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html' 12import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html'
13import { asyncMiddleware, disableRobots, embedCSP } from '../middlewares' 13import { asyncMiddleware, embedCSP } from '../middlewares'
14 14
15const clientsRouter = express.Router() 15const clientsRouter = express.Router()
16 16
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) {
16 'categoryOneOf', 16 'categoryOneOf',
17 'licenceOneOf', 17 'licenceOneOf',
18 'languageOneOf', 18 'languageOneOf',
19 'privacyOneOf',
19 'tagsOneOf', 20 'tagsOneOf',
20 'tagsAllOf', 21 'tagsAllOf',
21 'isLocal', 22 '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'
7import { getServerActor } from '@server/models/application/application' 7import { getServerActor } from '@server/models/application/application'
8import { ExpressPromiseHandler } from '@server/types/express' 8import { ExpressPromiseHandler } from '@server/types/express'
9import { MUserAccountId, MVideoFullLight } from '@server/types/models' 9import { MUserAccountId, MVideoFullLight } from '@server/types/models'
10import { getAllPrivacies } from '@shared/core-utils'
10import { VideoInclude } from '@shared/models' 11import { VideoInclude } from '@shared/models'
11import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared' 12import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared'
12import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' 13import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
@@ -487,6 +488,10 @@ const commonVideosFiltersValidator = [
487 .optional() 488 .optional()
488 .customSanitizer(toArray) 489 .customSanitizer(toArray)
489 .custom(isStringArray).withMessage('Should have a valid one of language array'), 490 .custom(isStringArray).withMessage('Should have a valid one of language array'),
491 query('privacyOneOf')
492 .optional()
493 .customSanitizer(toArray)
494 .custom(isNumberArray).withMessage('Should have a valid one of privacy array'),
490 query('tagsOneOf') 495 query('tagsOneOf')
491 .optional() 496 .optional()
492 .customSanitizer(toArray) 497 .customSanitizer(toArray)
@@ -536,10 +541,12 @@ const commonVideosFiltersValidator = [
536 // FIXME: deprecated in 4.0, to remove 541 // FIXME: deprecated in 4.0, to remove
537 { 542 {
538 if (req.query.filter === 'all-local') { 543 if (req.query.filter === 'all-local') {
539 req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY 544 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
540 req.query.isLocal = true 545 req.query.isLocal = true
546 req.query.privacyOneOf = getAllPrivacies()
541 } else if (req.query.filter === 'all') { 547 } else if (req.query.filter === 'all') {
542 req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY 548 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
549 req.query.privacyOneOf = getAllPrivacies()
543 } else if (req.query.filter === 'local') { 550 } else if (req.query.filter === 'local') {
544 req.query.isLocal = true 551 req.query.isLocal = true
545 } 552 }
@@ -550,7 +557,7 @@ const commonVideosFiltersValidator = [
550 const user = res.locals.oauth?.token.User 557 const user = res.locals.oauth?.token.User
551 558
552 if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) { 559 if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
553 if (req.query.include) { 560 if (req.query.include || req.query.privacyOneOf) {
554 return res.fail({ 561 return res.fail({
555 status: HttpStatusCode.UNAUTHORIZED_401, 562 status: HttpStatusCode.UNAUTHORIZED_401,
556 message: 'You are not allowed to see all videos.' 563 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 = {
40 languageOneOf?: string[] 40 languageOneOf?: string[]
41 tagsOneOf?: string[] 41 tagsOneOf?: string[]
42 tagsAllOf?: string[] 42 tagsAllOf?: string[]
43 privacyOneOf?: VideoPrivacy[]
43 44
44 uuids?: string[] 45 uuids?: string[]
45 46
@@ -138,11 +139,6 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
138 this.whereStateAvailable() 139 this.whereStateAvailable()
139 } 140 }
140 141
141 // Only list videos with the appropriate priavcy
142 if (!(options.include & VideoInclude.HIDDEN_PRIVACY)) {
143 this.wherePrivacyAvailable(options.user)
144 }
145
146 if (options.videoPlaylistId) { 142 if (options.videoPlaylistId) {
147 this.joinPlaylist(options.videoPlaylistId) 143 this.joinPlaylist(options.videoPlaylistId)
148 } 144 }
@@ -187,6 +183,13 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
187 this.whereTagsAllOf(options.tagsAllOf) 183 this.whereTagsAllOf(options.tagsAllOf)
188 } 184 }
189 185
186 if (options.privacyOneOf) {
187 this.wherePrivacyOneOf(options.privacyOneOf)
188 } else {
189 // Only list videos with the appropriate priavcy
190 this.wherePrivacyAvailable(options.user)
191 }
192
190 if (options.uuids) { 193 if (options.uuids) {
191 this.whereUUIDs(options.uuids) 194 this.whereUUIDs(options.uuids)
192 } 195 }
@@ -435,6 +438,11 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
435 ) 438 )
436 } 439 }
437 440
441 private wherePrivacyOneOf (privacyOneOf: VideoPrivacy[]) {
442 this.and.push('"video"."privacy" IN (:privacyOneOf)')
443 this.replacements.privacyOneOf = privacyOneOf
444 }
445
438 private whereUUIDs (uuids: string[]) { 446 private whereUUIDs (uuids: string[]) {
439 this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')') 447 this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')')
440 } 448 }
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<Partial<AttributesOnly<VideoModel>>> {
1041 languageOneOf?: string[] 1041 languageOneOf?: string[]
1042 tagsOneOf?: string[] 1042 tagsOneOf?: string[]
1043 tagsAllOf?: string[] 1043 tagsAllOf?: string[]
1044 privacyOneOf?: VideoPrivacy[]
1044 1045
1045 accountId?: number 1046 accountId?: number
1046 videoChannelId?: number 1047 videoChannelId?: number
@@ -1059,6 +1060,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1059 search?: string 1060 search?: string
1060 }) { 1061 }) {
1061 VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) 1062 VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
1063 VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
1062 1064
1063 const trendingDays = options.sort.endsWith('trending') 1065 const trendingDays = options.sort.endsWith('trending')
1064 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS 1066 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
@@ -1082,6 +1084,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1082 'languageOneOf', 1084 'languageOneOf',
1083 'tagsOneOf', 1085 'tagsOneOf',
1084 'tagsAllOf', 1086 'tagsAllOf',
1087 'privacyOneOf',
1085 'isLocal', 1088 'isLocal',
1086 'include', 1089 'include',
1087 'displayOnlyForFollower', 1090 'displayOnlyForFollower',
@@ -1119,6 +1122,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1119 languageOneOf?: string[] 1122 languageOneOf?: string[]
1120 tagsOneOf?: string[] 1123 tagsOneOf?: string[]
1121 tagsAllOf?: string[] 1124 tagsAllOf?: string[]
1125 privacyOneOf?: VideoPrivacy[]
1122 1126
1123 displayOnlyForFollower: DisplayOnlyForFollowerOptions | null 1127 displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
1124 1128
@@ -1140,6 +1144,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1140 uuids?: string[] 1144 uuids?: string[]
1141 }) { 1145 }) {
1142 VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) 1146 VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
1147 VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
1143 1148
1144 const serverActor = await getServerActor() 1149 const serverActor = await getServerActor()
1145 1150
@@ -1153,6 +1158,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1153 'languageOneOf', 1158 'languageOneOf',
1154 'tagsOneOf', 1159 'tagsOneOf',
1155 'tagsAllOf', 1160 'tagsAllOf',
1161 'privacyOneOf',
1156 'user', 1162 'user',
1157 'isLocal', 1163 'isLocal',
1158 'host', 1164 'host',
@@ -1510,14 +1516,19 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1510 1516
1511 private static throwIfPrivateIncludeWithoutUser (include: VideoInclude, user: MUserAccountId) { 1517 private static throwIfPrivateIncludeWithoutUser (include: VideoInclude, user: MUserAccountId) {
1512 if (VideoModel.isPrivateInclude(include) && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1518 if (VideoModel.isPrivateInclude(include) && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1513 throw new Error('Try to filter all-local but no user has not the see all videos right') 1519 throw new Error('Try to filter all-local but user cannot see all videos')
1520 }
1521 }
1522
1523 private static throwIfPrivacyOneOfWithoutUser (privacyOneOf: VideoPrivacy[], user: MUserAccountId) {
1524 if (privacyOneOf && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1525 throw new Error('Try to choose video privacies but user cannot see all videos')
1514 } 1526 }
1515 } 1527 }
1516 1528
1517 private static isPrivateInclude (include: VideoInclude) { 1529 private static isPrivateInclude (include: VideoInclude) {
1518 return include & VideoInclude.BLACKLISTED || 1530 return include & VideoInclude.BLACKLISTED ||
1519 include & VideoInclude.BLOCKED_OWNER || 1531 include & VideoInclude.BLOCKED_OWNER ||
1520 include & VideoInclude.HIDDEN_PRIVACY ||
1521 include & VideoInclude.NOT_PUBLISHED_STATE 1532 include & VideoInclude.NOT_PUBLISHED_STATE
1522 } 1533 }
1523 1534
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 {
9 setAccessTokensToServers, 9 setAccessTokensToServers,
10 setDefaultVideoChannel 10 setDefaultVideoChannel
11} from '@shared/extra-utils' 11} from '@shared/extra-utils'
12import { HttpStatusCode, UserRole, VideoInclude } from '@shared/models' 12import { HttpStatusCode, UserRole, VideoInclude, VideoPrivacy } from '@shared/models'
13 13
14describe('Test video filters validators', function () { 14describe('Test video filters validators', function () {
15 let server: PeerTubeServer 15 let server: PeerTubeServer
@@ -112,7 +112,7 @@ describe('Test video filters validators', function () {
112 112
113 const validIncludes = [ 113 const validIncludes = [
114 VideoInclude.NONE, 114 VideoInclude.NONE,
115 VideoInclude.HIDDEN_PRIVACY, 115 VideoInclude.BLOCKED_OWNER,
116 VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED 116 VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED
117 ] 117 ]
118 118
@@ -120,6 +120,7 @@ describe('Test video filters validators', function () {
120 token?: string 120 token?: string
121 isLocal?: boolean 121 isLocal?: boolean
122 include?: VideoInclude 122 include?: VideoInclude
123 privacyOneOf?: VideoPrivacy[]
123 expectedStatus: HttpStatusCode 124 expectedStatus: HttpStatusCode
124 }) { 125 }) {
125 const paths = [ 126 const paths = [
@@ -136,6 +137,7 @@ describe('Test video filters validators', function () {
136 token: options.token || server.accessToken, 137 token: options.token || server.accessToken,
137 query: { 138 query: {
138 isLocal: options.isLocal, 139 isLocal: options.isLocal,
140 privacyOneOf: options.privacyOneOf,
139 include: options.include 141 include: options.include
140 }, 142 },
141 expectedStatus: options.expectedStatus 143 expectedStatus: options.expectedStatus
@@ -143,6 +145,22 @@ describe('Test video filters validators', function () {
143 } 145 }
144 } 146 }
145 147
148 it('Should fail with a bad privacyOneOf', async function () {
149 await testEndpoints({ privacyOneOf: [ 'toto' ] as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
150 })
151
152 it('Should succeed with a good privacyOneOf', async function () {
153 await testEndpoints({ privacyOneOf: [ VideoPrivacy.INTERNAL ], expectedStatus: HttpStatusCode.OK_200 })
154 })
155
156 it('Should fail to use privacyOneOf with a simple user', async function () {
157 await testEndpoints({
158 privacyOneOf: [ VideoPrivacy.INTERNAL ],
159 token: userAccessToken,
160 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
161 })
162 })
163
146 it('Should fail with a bad include', async function () { 164 it('Should fail with a bad include', async function () {
147 await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 165 await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
148 }) 166 })
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 () {
138 hasWebtorrentFiles?: boolean 138 hasWebtorrentFiles?: boolean
139 hasHLSFiles?: boolean 139 hasHLSFiles?: boolean
140 include?: VideoInclude 140 include?: VideoInclude
141 privacyOneOf?: VideoPrivacy[]
141 category?: number 142 category?: number
142 tagsAllOf?: string[] 143 tagsAllOf?: string[]
143 token?: string 144 token?: string
@@ -148,7 +149,7 @@ describe('Test videos filter', function () {
148 path: options.path, 149 path: options.path,
149 token: options.token ?? options.server.accessToken, 150 token: options.token ?? options.server.accessToken,
150 query: { 151 query: {
151 ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles' ]), 152 ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles', 'privacyOneOf' ]),
152 153
153 sort: 'createdAt' 154 sort: 'createdAt'
154 }, 155 },
@@ -162,6 +163,7 @@ describe('Test videos filter', function () {
162 server: PeerTubeServer 163 server: PeerTubeServer
163 isLocal?: boolean 164 isLocal?: boolean
164 include?: VideoInclude 165 include?: VideoInclude
166 privacyOneOf?: VideoPrivacy[]
165 token?: string 167 token?: string
166 expectedStatus?: HttpStatusCode 168 expectedStatus?: HttpStatusCode
167 }) { 169 }) {
@@ -195,7 +197,7 @@ describe('Test videos filter', function () {
195 server, 197 server,
196 token, 198 token,
197 isLocal: true, 199 isLocal: true,
198 include: VideoInclude.HIDDEN_PRIVACY 200 privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ]
199 }) 201 })
200 202
201 for (const names of namesResults) { 203 for (const names of namesResults) {
@@ -216,7 +218,7 @@ describe('Test videos filter', function () {
216 const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({ 218 const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({
217 server, 219 server,
218 token, 220 token,
219 include: VideoInclude.HIDDEN_PRIVACY 221 privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ]
220 }) 222 })
221 223
222 expect(channelVideos).to.have.lengthOf(3) 224 expect(channelVideos).to.have.lengthOf(3)