]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/video-playlists.ts
Merge branch 'release/4.2.0' into develop
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / video-playlists.ts
CommitLineData
41fb13c3 1import express from 'express'
c8861d5d 2import { body, param, query, ValidationChain } from 'express-validator'
f8360396 3import { ExpressPromiseHandler } from '@server/types/express-handler'
ba5a8d89 4import { MUserAccountId } from '@server/types/models'
d17c7b4e
C
5import {
6 HttpStatusCode,
7 UserRight,
8 VideoPlaylistCreate,
9 VideoPlaylistPrivacy,
10 VideoPlaylistType,
11 VideoPlaylistUpdate
12} from '@shared/models'
c8861d5d
C
13import {
14 isArrayOf,
15 isIdOrUUIDValid,
16 isIdValid,
17 isUUIDValid,
d4a8e7a6 18 toCompleteUUID,
c8861d5d
C
19 toIntArray,
20 toIntOrNull,
21 toValueOrNull
22} from '../../../helpers/custom-validators/misc'
418d092a 23import {
9f79ade6 24 isVideoPlaylistDescriptionValid,
418d092a 25 isVideoPlaylistNameValid,
df0b219d
C
26 isVideoPlaylistPrivacyValid,
27 isVideoPlaylistTimestampValid,
28 isVideoPlaylistTypeValid
418d092a 29} from '../../../helpers/custom-validators/video-playlists'
c729caf6 30import { isVideoImageValid } from '../../../helpers/custom-validators/videos'
418d092a 31import { cleanUpReqFiles } from '../../../helpers/express-utils'
ba5a8d89 32import { logger } from '../../../helpers/logger'
ba5a8d89
C
33import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
34import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
26d6bf65 35import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
f43db2f4 36import { authenticatePromiseIfNeeded } from '../../auth'
d4a8e7a6
C
37import {
38 areValidationErrors,
39 doesVideoChannelIdExist,
40 doesVideoExist,
41 doesVideoPlaylistExist,
42 isValidPlaylistIdParam,
43 VideoPlaylistFetchType
44} from '../shared'
418d092a
C
45
46const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
1b319b7a
C
47 body('displayName')
48 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
49
418d092a
C
50 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
51 logger.debug('Checking videoPlaylistsAddValidator parameters', { parameters: req.body })
52
53 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
54
c5e4e36d 55 const body: VideoPlaylistCreate = req.body
0f6acda1 56 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
c5e4e36d 57
d4a8e7a6
C
58 if (
59 !body.videoChannelId &&
60 (body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED)
61 ) {
c5e4e36d 62 cleanUpReqFiles(req)
76148b27 63
d4a8e7a6 64 return res.fail({ message: 'Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.' })
c5e4e36d 65 }
418d092a
C
66
67 return next()
68 }
69])
70
71const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
d4a8e7a6 72 isValidPlaylistIdParam('playlistId'),
418d092a 73
1b319b7a
C
74 body('displayName')
75 .optional()
76 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
77
418d092a
C
78 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
79 logger.debug('Checking videoPlaylistsUpdateValidator parameters', { parameters: req.body })
80
81 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
82
0f6acda1 83 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
07b1a18a 84
453e83ea 85 const videoPlaylist = getPlaylist(res)
07b1a18a 86
453e83ea 87 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
418d092a
C
88 return cleanUpReqFiles(req)
89 }
90
c5e4e36d
C
91 const body: VideoPlaylistUpdate = req.body
92
c5e4e36d
C
93 const newPrivacy = body.privacy || videoPlaylist.privacy
94 if (newPrivacy === VideoPlaylistPrivacy.PUBLIC &&
95 (
96 (!videoPlaylist.videoChannelId && !body.videoChannelId) ||
97 body.videoChannelId === null
98 )
99 ) {
100 cleanUpReqFiles(req)
76148b27
RK
101
102 return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
c5e4e36d
C
103 }
104
df0b219d
C
105 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
106 cleanUpReqFiles(req)
76148b27
RK
107
108 return res.fail({ message: 'Cannot update a watch later playlist.' })
df0b219d
C
109 }
110
0f6acda1 111 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
418d092a
C
112
113 return next()
114 }
115])
116
117const videoPlaylistsDeleteValidator = [
d4a8e7a6 118 isValidPlaylistIdParam('playlistId'),
418d092a
C
119
120 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
121 logger.debug('Checking videoPlaylistsDeleteValidator parameters', { parameters: req.params })
122
123 if (areValidationErrors(req, res)) return
124
0f6acda1 125 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
df0b219d 126
453e83ea 127 const videoPlaylist = getPlaylist(res)
df0b219d 128 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
76148b27 129 return res.fail({ message: 'Cannot delete a watch later playlist.' })
df0b219d
C
130 }
131
453e83ea 132 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
418d092a
C
133 return
134 }
135
136 return next()
137 }
138]
139
453e83ea
C
140const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
141 return [
d4a8e7a6 142 isValidPlaylistIdParam('playlistId'),
418d092a 143
453e83ea
C
144 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
145 logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params })
418d092a 146
453e83ea 147 if (areValidationErrors(req, res)) return
418d092a 148
453e83ea 149 if (!await doesVideoPlaylistExist(req.params.playlistId, res, fetchType)) return
418d092a 150
453e83ea 151 const videoPlaylist = res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
07b1a18a 152
453e83ea
C
153 // Video is unlisted, check we used the uuid to fetch it
154 if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
155 if (isUUIDValid(req.params.playlistId)) return next()
07b1a18a 156
76148b27
RK
157 return res.fail({
158 status: HttpStatusCode.NOT_FOUND_404,
159 message: 'Playlist not found'
160 })
453e83ea 161 }
07b1a18a 162
453e83ea
C
163 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
164 await authenticatePromiseIfNeeded(req, res)
418d092a 165
453e83ea 166 const user = res.locals.oauth ? res.locals.oauth.token.User : null
4d09cfba 167
453e83ea
C
168 if (
169 !user ||
170 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
171 ) {
76148b27
RK
172 return res.fail({
173 status: HttpStatusCode.FORBIDDEN_403,
174 message: 'Cannot get this private video playlist.'
175 })
453e83ea
C
176 }
177
178 return next()
418d092a
C
179 }
180
181 return next()
182 }
453e83ea
C
183 ]
184}
418d092a 185
c06af501
RK
186const videoPlaylistsSearchValidator = [
187 query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
188
189 (req: express.Request, res: express.Response, next: express.NextFunction) => {
190 logger.debug('Checking videoPlaylists search query', { parameters: req.query })
191
192 if (areValidationErrors(req, res)) return
193
194 return next()
195 }
196]
197
418d092a 198const videoPlaylistsAddVideoValidator = [
d4a8e7a6
C
199 isValidPlaylistIdParam('playlistId'),
200
418d092a 201 body('videoId')
d4a8e7a6 202 .customSanitizer(toCompleteUUID)
418d092a
C
203 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'),
204 body('startTimestamp')
205 .optional()
df0b219d 206 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
418d092a
C
207 body('stopTimestamp')
208 .optional()
df0b219d 209 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
418d092a
C
210
211 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
212 logger.debug('Checking videoPlaylistsAddVideoValidator parameters', { parameters: req.params })
213
214 if (areValidationErrors(req, res)) return
215
0f6acda1
C
216 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
217 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
418d092a 218
453e83ea 219 const videoPlaylist = getPlaylist(res)
418d092a 220
453e83ea 221 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
418d092a
C
222 return
223 }
224
225 return next()
226 }
227]
228
229const videoPlaylistsUpdateOrRemoveVideoValidator = [
d4a8e7a6 230 isValidPlaylistIdParam('playlistId'),
bfbd9128 231 param('playlistElementId')
d4a8e7a6 232 .customSanitizer(toCompleteUUID)
bfbd9128 233 .custom(isIdValid).withMessage('Should have an element id/uuid'),
418d092a
C
234 body('startTimestamp')
235 .optional()
df0b219d 236 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
418d092a
C
237 body('stopTimestamp')
238 .optional()
df0b219d 239 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
418d092a
C
240
241 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
242 logger.debug('Checking videoPlaylistsRemoveVideoValidator parameters', { parameters: req.params })
243
244 if (areValidationErrors(req, res)) return
245
0f6acda1 246 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
418d092a 247
453e83ea 248 const videoPlaylist = getPlaylist(res)
418d092a 249
bfbd9128 250 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
418d092a 251 if (!videoPlaylistElement) {
76148b27
RK
252 res.fail({
253 status: HttpStatusCode.NOT_FOUND_404,
254 message: 'Video playlist element not found'
255 })
418d092a
C
256 return
257 }
258 res.locals.videoPlaylistElement = videoPlaylistElement
259
260 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
261
262 return next()
263 }
264]
265
266const videoPlaylistElementAPGetValidator = [
d4a8e7a6 267 isValidPlaylistIdParam('playlistId'),
37190663
C
268 param('playlistElementId')
269 .custom(isIdValid).withMessage('Should have an playlist element id'),
418d092a
C
270
271 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
272 logger.debug('Checking videoPlaylistElementAPGetValidator parameters', { parameters: req.params })
273
274 if (areValidationErrors(req, res)) return
275
37190663
C
276 const playlistElementId = parseInt(req.params.playlistElementId + '', 10)
277 const playlistId = req.params.playlistId
278
279 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
418d092a 280 if (!videoPlaylistElement) {
76148b27
RK
281 res.fail({
282 status: HttpStatusCode.NOT_FOUND_404,
283 message: 'Video playlist element not found'
284 })
418d092a
C
285 return
286 }
287
288 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
76148b27
RK
289 return res.fail({
290 status: HttpStatusCode.FORBIDDEN_403,
291 message: 'Cannot get this private video playlist.'
292 })
418d092a
C
293 }
294
b5fecbf4 295 res.locals.videoPlaylistElementAP = videoPlaylistElement
418d092a
C
296
297 return next()
298 }
299]
300
301const videoPlaylistsReorderVideosValidator = [
d4a8e7a6 302 isValidPlaylistIdParam('playlistId'),
418d092a
C
303 body('startPosition')
304 .isInt({ min: 1 }).withMessage('Should have a valid start position'),
305 body('insertAfterPosition')
306 .isInt({ min: 0 }).withMessage('Should have a valid insert after position'),
307 body('reorderLength')
308 .optional()
309 .isInt({ min: 1 }).withMessage('Should have a valid range length'),
310
311 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
312 logger.debug('Checking videoPlaylistsReorderVideosValidator parameters', { parameters: req.params })
313
314 if (areValidationErrors(req, res)) return
315
0f6acda1 316 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
418d092a 317
453e83ea 318 const videoPlaylist = getPlaylist(res)
418d092a
C
319 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
320
07b1a18a
C
321 const nextPosition = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id)
322 const startPosition: number = req.body.startPosition
323 const insertAfterPosition: number = req.body.insertAfterPosition
324 const reorderLength: number = req.body.reorderLength
325
326 if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
76148b27 327 res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
07b1a18a
C
328 return
329 }
330
331 if (reorderLength && reorderLength + startPosition > nextPosition) {
76148b27 332 res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
07b1a18a
C
333 return
334 }
335
418d092a
C
336 return next()
337 }
338]
339
df0b219d
C
340const commonVideoPlaylistFiltersValidator = [
341 query('playlistType')
342 .optional()
343 .custom(isVideoPlaylistTypeValid).withMessage('Should have a valid playlist type'),
344
345 (req: express.Request, res: express.Response, next: express.NextFunction) => {
346 logger.debug('Checking commonVideoPlaylistFiltersValidator parameters', { parameters: req.params })
347
348 if (areValidationErrors(req, res)) return
349
350 return next()
351 }
352]
353
f0a39880
C
354const doVideosInPlaylistExistValidator = [
355 query('videoIds')
356 .customSanitizer(toIntArray)
357 .custom(v => isArrayOf(v, isIdValid)).withMessage('Should have a valid video ids array'),
358
359 (req: express.Request, res: express.Response, next: express.NextFunction) => {
360 logger.debug('Checking areVideosInPlaylistExistValidator parameters', { parameters: req.query })
361
362 if (areValidationErrors(req, res)) return
363
364 return next()
365 }
366]
367
418d092a
C
368// ---------------------------------------------------------------------------
369
370export {
371 videoPlaylistsAddValidator,
372 videoPlaylistsUpdateValidator,
373 videoPlaylistsDeleteValidator,
374 videoPlaylistsGetValidator,
c06af501 375 videoPlaylistsSearchValidator,
418d092a
C
376
377 videoPlaylistsAddVideoValidator,
378 videoPlaylistsUpdateOrRemoveVideoValidator,
379 videoPlaylistsReorderVideosValidator,
380
df0b219d
C
381 videoPlaylistElementAPGetValidator,
382
f0a39880
C
383 commonVideoPlaylistFiltersValidator,
384
385 doVideosInPlaylistExistValidator
418d092a
C
386}
387
388// ---------------------------------------------------------------------------
389
390function getCommonPlaylistEditAttributes () {
391 return [
392 body('thumbnailfile')
c729caf6 393 .custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile'))
a1587156
C
394 .withMessage(
395 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
396 CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
397 ),
418d092a 398
418d092a
C
399 body('description')
400 .optional()
401 .customSanitizer(toValueOrNull)
402 .custom(isVideoPlaylistDescriptionValid).withMessage('Should have a valid description'),
403 body('privacy')
404 .optional()
c8861d5d 405 .customSanitizer(toIntOrNull)
418d092a
C
406 .custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
407 body('videoChannelId')
408 .optional()
c8861d5d 409 .customSanitizer(toIntOrNull)
ba5a8d89 410 ] as (ValidationChain | ExpressPromiseHandler)[]
418d092a
C
411}
412
453e83ea 413function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
418d092a 414 if (videoPlaylist.isOwned() === false) {
76148b27
RK
415 res.fail({
416 status: HttpStatusCode.FORBIDDEN_403,
417 message: 'Cannot manage video playlist of another server.'
418 })
418d092a
C
419 return false
420 }
421
422 // Check if the user can manage the video playlist
423 // The user can delete it if s/he is an admin
424 // Or if s/he is the video playlist's owner
425 if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) {
76148b27
RK
426 res.fail({
427 status: HttpStatusCode.FORBIDDEN_403,
428 message: 'Cannot manage video playlist of another user'
429 })
418d092a
C
430 return false
431 }
432
433 return true
434}
453e83ea
C
435
436function getPlaylist (res: express.Response) {
437 return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
438}