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