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