]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - 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
1import * as express from 'express'
2import { body, param, query, ValidationChain } from 'express-validator'
3import { UserRight, VideoPlaylistCreate, VideoPlaylistUpdate } from '../../../../shared'
4import { logger } from '../../../helpers/logger'
5import { areValidationErrors } from '../utils'
6import { isVideoImage } from '../../../helpers/custom-validators/videos'
7import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
8import {
9 isArrayOf,
10 isIdOrUUIDValid,
11 isIdValid,
12 isUUIDValid,
13 toIntArray,
14 toIntOrNull,
15 toValueOrNull
16} from '../../../helpers/custom-validators/misc'
17import {
18 isVideoPlaylistDescriptionValid,
19 isVideoPlaylistNameValid,
20 isVideoPlaylistPrivacyValid,
21 isVideoPlaylistTimestampValid,
22 isVideoPlaylistTypeValid
23} from '../../../helpers/custom-validators/video-playlists'
24import { cleanUpReqFiles } from '../../../helpers/express-utils'
25import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
26import { authenticatePromiseIfNeeded } from '../../oauth'
27import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
28import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
29import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoPlaylistFetchType } from '../../../helpers/middlewares'
30import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
31import { MUserAccountId } from '@server/types/models'
32import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
33
34const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
35 body('displayName')
36 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
37
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
43 const body: VideoPlaylistCreate = req.body
44 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
45
46 if (body.privacy === VideoPlaylistPrivacy.PUBLIC && !body.videoChannelId) {
47 cleanUpReqFiles(req)
48 return res.status(HttpStatusCode.BAD_REQUEST_400)
49 .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' })
50 }
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
60 body('displayName')
61 .optional()
62 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
63
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
69 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
70
71 const videoPlaylist = getPlaylist(res)
72
73 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
74 return cleanUpReqFiles(req)
75 }
76
77 const body: VideoPlaylistUpdate = req.body
78
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)
87 return res.status(HttpStatusCode.BAD_REQUEST_400)
88 .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' })
89 }
90
91 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
92 cleanUpReqFiles(req)
93 return res.status(HttpStatusCode.BAD_REQUEST_400)
94 .json({ error: 'Cannot update a watch later playlist.' })
95 }
96
97 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
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
112 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
113
114 const videoPlaylist = getPlaylist(res)
115 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
116 return res.status(HttpStatusCode.BAD_REQUEST_400)
117 .json({ error: 'Cannot delete a watch later playlist.' })
118 }
119
120 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
121 return
122 }
123
124 return next()
125 }
126]
127
128const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
129 return [
130 param('playlistId')
131 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
132
133 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
134 logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params })
135
136 if (areValidationErrors(req, res)) return
137
138 if (!await doesVideoPlaylistExist(req.params.playlistId, res, fetchType)) return
139
140 const videoPlaylist = res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
141
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()
145
146 return res.status(HttpStatusCode.NOT_FOUND_404).end()
147 }
148
149 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
150 await authenticatePromiseIfNeeded(req, res)
151
152 const user = res.locals.oauth ? res.locals.oauth.token.User : null
153
154 if (
155 !user ||
156 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
157 ) {
158 return res.status(HttpStatusCode.FORBIDDEN_403)
159 .json({ error: 'Cannot get this private video playlist.' })
160 }
161
162 return next()
163 }
164
165 return next()
166 }
167 ]
168}
169
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
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()
189 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
190 body('stopTimestamp')
191 .optional()
192 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
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
199 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
200 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
201
202 const videoPlaylist = getPlaylist(res)
203
204 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
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'),
215 param('playlistElementId')
216 .custom(isIdValid).withMessage('Should have an element id/uuid'),
217 body('startTimestamp')
218 .optional()
219 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
220 body('stopTimestamp')
221 .optional()
222 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
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
229 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
230
231 const videoPlaylist = getPlaylist(res)
232
233 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
234 if (!videoPlaylistElement) {
235 res.status(HttpStatusCode.NOT_FOUND_404)
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'),
252 param('playlistElementId')
253 .custom(isIdValid).withMessage('Should have an playlist element id'),
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
260 const playlistElementId = parseInt(req.params.playlistElementId + '', 10)
261 const playlistId = req.params.playlistId
262
263 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
264 if (!videoPlaylistElement) {
265 res.status(HttpStatusCode.NOT_FOUND_404)
266 .json({ error: 'Video playlist element not found' })
267 .end()
268
269 return
270 }
271
272 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
273 return res.status(HttpStatusCode.FORBIDDEN_403).end()
274 }
275
276 res.locals.videoPlaylistElementAP = videoPlaylistElement
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
298 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
299
300 const videoPlaylist = getPlaylist(res)
301 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
302
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) {
309 res.status(HttpStatusCode.BAD_REQUEST_400)
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) {
317 res.status(HttpStatusCode.BAD_REQUEST_400)
318 .json({ error: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
319 .end()
320
321 return
322 }
323
324 return next()
325 }
326]
327
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
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
356// ---------------------------------------------------------------------------
357
358export {
359 videoPlaylistsAddValidator,
360 videoPlaylistsUpdateValidator,
361 videoPlaylistsDeleteValidator,
362 videoPlaylistsGetValidator,
363 videoPlaylistsSearchValidator,
364
365 videoPlaylistsAddVideoValidator,
366 videoPlaylistsUpdateOrRemoveVideoValidator,
367 videoPlaylistsReorderVideosValidator,
368
369 videoPlaylistElementAPGetValidator,
370
371 commonVideoPlaylistFiltersValidator,
372
373 doVideosInPlaylistExistValidator
374}
375
376// ---------------------------------------------------------------------------
377
378function getCommonPlaylistEditAttributes () {
379 return [
380 body('thumbnailfile')
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 ),
386
387 body('description')
388 .optional()
389 .customSanitizer(toValueOrNull)
390 .custom(isVideoPlaylistDescriptionValid).withMessage('Should have a valid description'),
391 body('privacy')
392 .optional()
393 .customSanitizer(toIntOrNull)
394 .custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
395 body('videoChannelId')
396 .optional()
397 .customSanitizer(toIntOrNull)
398 ] as (ValidationChain | express.Handler)[]
399}
400
401function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
402 if (videoPlaylist.isOwned() === false) {
403 res.status(HttpStatusCode.FORBIDDEN_403)
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) {
414 res.status(HttpStatusCode.FORBIDDEN_403)
415 .json({ error: 'Cannot manage video playlist of another user' })
416 .end()
417
418 return false
419 }
420
421 return true
422}
423
424function getPlaylist (res: express.Response) {
425 return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
426}