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