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