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