]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - server/middlewares/validators/videos/video-playlists.ts
Fix big play button
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / video-playlists.ts
... / ...
CommitLineData
1import * as express from 'express'
2import { body, param, query, ValidationChain } from 'express-validator'
3import { ExpressPromiseHandler } from '@server/types/express'
4import { MUserAccountId } from '@server/types/models'
5import { UserRight, VideoPlaylistCreate, VideoPlaylistUpdate } from '../../../../shared'
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'
9import {
10 isArrayOf,
11 isIdOrUUIDValid,
12 isIdValid,
13 isUUIDValid,
14 toIntArray,
15 toIntOrNull,
16 toValueOrNull
17} from '../../../helpers/custom-validators/misc'
18import {
19 isVideoPlaylistDescriptionValid,
20 isVideoPlaylistNameValid,
21 isVideoPlaylistPrivacyValid,
22 isVideoPlaylistTimestampValid,
23 isVideoPlaylistTypeValid
24} from '../../../helpers/custom-validators/video-playlists'
25import { isVideoImage } from '../../../helpers/custom-validators/videos'
26import { cleanUpReqFiles } from '../../../helpers/express-utils'
27import { logger } from '../../../helpers/logger'
28import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
29import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
30import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
31import { authenticatePromiseIfNeeded } from '../../auth'
32import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoPlaylistFetchType } from '../shared'
33
34const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
35 body('displayName')
36 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
37
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
43 const body: VideoPlaylistCreate = req.body
44 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
45
46 if (body.privacy === VideoPlaylistPrivacy.PUBLIC && !body.videoChannelId) {
47 cleanUpReqFiles(req)
48
49 return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
50 }
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
60 body('displayName')
61 .optional()
62 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
63
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
69 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
70
71 const videoPlaylist = getPlaylist(res)
72
73 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
74 return cleanUpReqFiles(req)
75 }
76
77 const body: VideoPlaylistUpdate = req.body
78
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)
87
88 return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
89 }
90
91 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
92 cleanUpReqFiles(req)
93
94 return res.fail({ message: 'Cannot update a watch later playlist.' })
95 }
96
97 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
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
112 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
113
114 const videoPlaylist = getPlaylist(res)
115 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
116 return res.fail({ message: '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
127const 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.fail({
146 status: HttpStatusCode.NOT_FOUND_404,
147 message: 'Playlist not found'
148 })
149 }
150
151 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
152 await authenticatePromiseIfNeeded(req, res)
153
154 const user = res.locals.oauth ? res.locals.oauth.token.User : null
155
156 if (
157 !user ||
158 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
159 ) {
160 return res.fail({
161 status: HttpStatusCode.FORBIDDEN_403,
162 message: 'Cannot get this private video playlist.'
163 })
164 }
165
166 return next()
167 }
168
169 return next()
170 }
171 ]
172}
173
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
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()
193 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
194 body('stopTimestamp')
195 .optional()
196 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
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
203 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
204 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
205
206 const videoPlaylist = getPlaylist(res)
207
208 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
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'),
219 param('playlistElementId')
220 .custom(isIdValid).withMessage('Should have an element id/uuid'),
221 body('startTimestamp')
222 .optional()
223 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
224 body('stopTimestamp')
225 .optional()
226 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
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
233 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
234
235 const videoPlaylist = getPlaylist(res)
236
237 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
238 if (!videoPlaylistElement) {
239 res.fail({
240 status: HttpStatusCode.NOT_FOUND_404,
241 message: 'Video playlist element not found'
242 })
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'),
256 param('playlistElementId')
257 .custom(isIdValid).withMessage('Should have an playlist element id'),
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
264 const playlistElementId = parseInt(req.params.playlistElementId + '', 10)
265 const playlistId = req.params.playlistId
266
267 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
268 if (!videoPlaylistElement) {
269 res.fail({
270 status: HttpStatusCode.NOT_FOUND_404,
271 message: 'Video playlist element not found'
272 })
273 return
274 }
275
276 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
277 return res.fail({
278 status: HttpStatusCode.FORBIDDEN_403,
279 message: 'Cannot get this private video playlist.'
280 })
281 }
282
283 res.locals.videoPlaylistElementAP = videoPlaylistElement
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
305 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
306
307 const videoPlaylist = getPlaylist(res)
308 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
309
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) {
316 res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
317 return
318 }
319
320 if (reorderLength && reorderLength + startPosition > nextPosition) {
321 res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
322 return
323 }
324
325 return next()
326 }
327]
328
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
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
357// ---------------------------------------------------------------------------
358
359export {
360 videoPlaylistsAddValidator,
361 videoPlaylistsUpdateValidator,
362 videoPlaylistsDeleteValidator,
363 videoPlaylistsGetValidator,
364 videoPlaylistsSearchValidator,
365
366 videoPlaylistsAddVideoValidator,
367 videoPlaylistsUpdateOrRemoveVideoValidator,
368 videoPlaylistsReorderVideosValidator,
369
370 videoPlaylistElementAPGetValidator,
371
372 commonVideoPlaylistFiltersValidator,
373
374 doVideosInPlaylistExistValidator
375}
376
377// ---------------------------------------------------------------------------
378
379function getCommonPlaylistEditAttributes () {
380 return [
381 body('thumbnailfile')
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 ),
387
388 body('description')
389 .optional()
390 .customSanitizer(toValueOrNull)
391 .custom(isVideoPlaylistDescriptionValid).withMessage('Should have a valid description'),
392 body('privacy')
393 .optional()
394 .customSanitizer(toIntOrNull)
395 .custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
396 body('videoChannelId')
397 .optional()
398 .customSanitizer(toIntOrNull)
399 ] as (ValidationChain | ExpressPromiseHandler)[]
400}
401
402function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
403 if (videoPlaylist.isOwned() === false) {
404 res.fail({
405 status: HttpStatusCode.FORBIDDEN_403,
406 message: 'Cannot manage video playlist of another server.'
407 })
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) {
415 res.fail({
416 status: HttpStatusCode.FORBIDDEN_403,
417 message: 'Cannot manage video playlist of another user'
418 })
419 return false
420 }
421
422 return true
423}
424
425function getPlaylist (res: express.Response) {
426 return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
427}