]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos/video-playlists.ts
Fix redis connection timeout
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / video-playlists.ts
1 import express from 'express'
2 import { body, param, query, ValidationChain } from 'express-validator'
3 import { ExpressPromiseHandler } from '@server/types/express'
4 import { MUserAccountId } from '@server/types/models'
5 import {
6 HttpStatusCode,
7 UserRight,
8 VideoPlaylistCreate,
9 VideoPlaylistPrivacy,
10 VideoPlaylistType,
11 VideoPlaylistUpdate
12 } from '@shared/models'
13 import {
14 isArrayOf,
15 isIdOrUUIDValid,
16 isIdValid,
17 isUUIDValid,
18 toCompleteUUID,
19 toIntArray,
20 toIntOrNull,
21 toValueOrNull
22 } from '../../../helpers/custom-validators/misc'
23 import {
24 isVideoPlaylistDescriptionValid,
25 isVideoPlaylistNameValid,
26 isVideoPlaylistPrivacyValid,
27 isVideoPlaylistTimestampValid,
28 isVideoPlaylistTypeValid
29 } from '../../../helpers/custom-validators/video-playlists'
30 import { isVideoImage } from '../../../helpers/custom-validators/videos'
31 import { cleanUpReqFiles } from '../../../helpers/express-utils'
32 import { logger } from '../../../helpers/logger'
33 import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
34 import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
35 import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
36 import { authenticatePromiseIfNeeded } from '../../auth'
37 import {
38 areValidationErrors,
39 doesVideoChannelIdExist,
40 doesVideoExist,
41 doesVideoPlaylistExist,
42 isValidPlaylistIdParam,
43 VideoPlaylistFetchType
44 } from '../shared'
45
46 const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
47 body('displayName')
48 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
49
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
55 const body: VideoPlaylistCreate = req.body
56 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
57
58 if (
59 !body.videoChannelId &&
60 (body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED)
61 ) {
62 cleanUpReqFiles(req)
63
64 return res.fail({ message: 'Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.' })
65 }
66
67 return next()
68 }
69 ])
70
71 const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
72 isValidPlaylistIdParam('playlistId'),
73
74 body('displayName')
75 .optional()
76 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
77
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
83 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
84
85 const videoPlaylist = getPlaylist(res)
86
87 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
88 return cleanUpReqFiles(req)
89 }
90
91 const body: VideoPlaylistUpdate = req.body
92
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)
101
102 return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
103 }
104
105 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
106 cleanUpReqFiles(req)
107
108 return res.fail({ message: 'Cannot update a watch later playlist.' })
109 }
110
111 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
112
113 return next()
114 }
115 ])
116
117 const videoPlaylistsDeleteValidator = [
118 isValidPlaylistIdParam('playlistId'),
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
125 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
126
127 const videoPlaylist = getPlaylist(res)
128 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
129 return res.fail({ message: 'Cannot delete a watch later playlist.' })
130 }
131
132 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
133 return
134 }
135
136 return next()
137 }
138 ]
139
140 const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
141 return [
142 isValidPlaylistIdParam('playlistId'),
143
144 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
145 logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params })
146
147 if (areValidationErrors(req, res)) return
148
149 if (!await doesVideoPlaylistExist(req.params.playlistId, res, fetchType)) return
150
151 const videoPlaylist = res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
152
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()
156
157 return res.fail({
158 status: HttpStatusCode.NOT_FOUND_404,
159 message: 'Playlist not found'
160 })
161 }
162
163 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
164 await authenticatePromiseIfNeeded(req, res)
165
166 const user = res.locals.oauth ? res.locals.oauth.token.User : null
167
168 if (
169 !user ||
170 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
171 ) {
172 return res.fail({
173 status: HttpStatusCode.FORBIDDEN_403,
174 message: 'Cannot get this private video playlist.'
175 })
176 }
177
178 return next()
179 }
180
181 return next()
182 }
183 ]
184 }
185
186 const 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
198 const videoPlaylistsAddVideoValidator = [
199 isValidPlaylistIdParam('playlistId'),
200
201 body('videoId')
202 .customSanitizer(toCompleteUUID)
203 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'),
204 body('startTimestamp')
205 .optional()
206 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
207 body('stopTimestamp')
208 .optional()
209 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
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
216 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
217 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
218
219 const videoPlaylist = getPlaylist(res)
220
221 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
222 return
223 }
224
225 return next()
226 }
227 ]
228
229 const videoPlaylistsUpdateOrRemoveVideoValidator = [
230 isValidPlaylistIdParam('playlistId'),
231 param('playlistElementId')
232 .customSanitizer(toCompleteUUID)
233 .custom(isIdValid).withMessage('Should have an element id/uuid'),
234 body('startTimestamp')
235 .optional()
236 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
237 body('stopTimestamp')
238 .optional()
239 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
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
246 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
247
248 const videoPlaylist = getPlaylist(res)
249
250 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
251 if (!videoPlaylistElement) {
252 res.fail({
253 status: HttpStatusCode.NOT_FOUND_404,
254 message: 'Video playlist element not found'
255 })
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
266 const videoPlaylistElementAPGetValidator = [
267 isValidPlaylistIdParam('playlistId'),
268 param('playlistElementId')
269 .custom(isIdValid).withMessage('Should have an playlist element id'),
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
276 const playlistElementId = parseInt(req.params.playlistElementId + '', 10)
277 const playlistId = req.params.playlistId
278
279 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
280 if (!videoPlaylistElement) {
281 res.fail({
282 status: HttpStatusCode.NOT_FOUND_404,
283 message: 'Video playlist element not found'
284 })
285 return
286 }
287
288 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
289 return res.fail({
290 status: HttpStatusCode.FORBIDDEN_403,
291 message: 'Cannot get this private video playlist.'
292 })
293 }
294
295 res.locals.videoPlaylistElementAP = videoPlaylistElement
296
297 return next()
298 }
299 ]
300
301 const videoPlaylistsReorderVideosValidator = [
302 isValidPlaylistIdParam('playlistId'),
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
316 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
317
318 const videoPlaylist = getPlaylist(res)
319 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
320
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) {
327 res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
328 return
329 }
330
331 if (reorderLength && reorderLength + startPosition > nextPosition) {
332 res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
333 return
334 }
335
336 return next()
337 }
338 ]
339
340 const 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
354 const 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
368 // ---------------------------------------------------------------------------
369
370 export {
371 videoPlaylistsAddValidator,
372 videoPlaylistsUpdateValidator,
373 videoPlaylistsDeleteValidator,
374 videoPlaylistsGetValidator,
375 videoPlaylistsSearchValidator,
376
377 videoPlaylistsAddVideoValidator,
378 videoPlaylistsUpdateOrRemoveVideoValidator,
379 videoPlaylistsReorderVideosValidator,
380
381 videoPlaylistElementAPGetValidator,
382
383 commonVideoPlaylistFiltersValidator,
384
385 doVideosInPlaylistExistValidator
386 }
387
388 // ---------------------------------------------------------------------------
389
390 function getCommonPlaylistEditAttributes () {
391 return [
392 body('thumbnailfile')
393 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile'))
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 ),
398
399 body('description')
400 .optional()
401 .customSanitizer(toValueOrNull)
402 .custom(isVideoPlaylistDescriptionValid).withMessage('Should have a valid description'),
403 body('privacy')
404 .optional()
405 .customSanitizer(toIntOrNull)
406 .custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
407 body('videoChannelId')
408 .optional()
409 .customSanitizer(toIntOrNull)
410 ] as (ValidationChain | ExpressPromiseHandler)[]
411 }
412
413 function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
414 if (videoPlaylist.isOwned() === false) {
415 res.fail({
416 status: HttpStatusCode.FORBIDDEN_403,
417 message: 'Cannot manage video playlist of another server.'
418 })
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) {
426 res.fail({
427 status: HttpStatusCode.FORBIDDEN_403,
428 message: 'Cannot manage video playlist of another user'
429 })
430 return false
431 }
432
433 return true
434 }
435
436 function getPlaylist (res: express.Response) {
437 return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
438 }