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