]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/video-playlists.ts
fix plugin storage return value when storing a Json array
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / video-playlists.ts
CommitLineData
41fb13c3 1import 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'
c0e8b12e 6import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
ba5a8d89
C
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,
d4a8e7a6 14 toCompleteUUID,
c8861d5d
C
15 toIntArray,
16 toIntOrNull,
17 toValueOrNull
18} from '../../../helpers/custom-validators/misc'
418d092a 19import {
9f79ade6 20 isVideoPlaylistDescriptionValid,
418d092a 21 isVideoPlaylistNameValid,
df0b219d
C
22 isVideoPlaylistPrivacyValid,
23 isVideoPlaylistTimestampValid,
24 isVideoPlaylistTypeValid
418d092a 25} from '../../../helpers/custom-validators/video-playlists'
ba5a8d89 26import { isVideoImage } from '../../../helpers/custom-validators/videos'
418d092a 27import { cleanUpReqFiles } from '../../../helpers/express-utils'
ba5a8d89 28import { logger } from '../../../helpers/logger'
ba5a8d89
C
29import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
30import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
26d6bf65 31import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
f43db2f4 32import { authenticatePromiseIfNeeded } from '../../auth'
d4a8e7a6
C
33import {
34 areValidationErrors,
35 doesVideoChannelIdExist,
36 doesVideoExist,
37 doesVideoPlaylistExist,
38 isValidPlaylistIdParam,
39 VideoPlaylistFetchType
40} from '../shared'
418d092a
C
41
42const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
1b319b7a
C
43 body('displayName')
44 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
45
418d092a
C
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
c5e4e36d 51 const body: VideoPlaylistCreate = req.body
0f6acda1 52 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
c5e4e36d 53
d4a8e7a6
C
54 if (
55 !body.videoChannelId &&
56 (body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED)
57 ) {
c5e4e36d 58 cleanUpReqFiles(req)
76148b27 59
d4a8e7a6 60 return res.fail({ message: 'Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.' })
c5e4e36d 61 }
418d092a
C
62
63 return next()
64 }
65])
66
67const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
d4a8e7a6 68 isValidPlaylistIdParam('playlistId'),
418d092a 69
1b319b7a
C
70 body('displayName')
71 .optional()
72 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
73
418d092a
C
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
0f6acda1 79 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
07b1a18a 80
453e83ea 81 const videoPlaylist = getPlaylist(res)
07b1a18a 82
453e83ea 83 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
418d092a
C
84 return cleanUpReqFiles(req)
85 }
86
c5e4e36d
C
87 const body: VideoPlaylistUpdate = req.body
88
c5e4e36d
C
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)
76148b27
RK
97
98 return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
c5e4e36d
C
99 }
100
df0b219d
C
101 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
102 cleanUpReqFiles(req)
76148b27
RK
103
104 return res.fail({ message: 'Cannot update a watch later playlist.' })
df0b219d
C
105 }
106
0f6acda1 107 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
418d092a
C
108
109 return next()
110 }
111])
112
113const videoPlaylistsDeleteValidator = [
d4a8e7a6 114 isValidPlaylistIdParam('playlistId'),
418d092a
C
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
0f6acda1 121 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
df0b219d 122
453e83ea 123 const videoPlaylist = getPlaylist(res)
df0b219d 124 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
76148b27 125 return res.fail({ message: 'Cannot delete a watch later playlist.' })
df0b219d
C
126 }
127
453e83ea 128 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
418d092a
C
129 return
130 }
131
132 return next()
133 }
134]
135
453e83ea
C
136const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
137 return [
d4a8e7a6 138 isValidPlaylistIdParam('playlistId'),
418d092a 139
453e83ea
C
140 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
141 logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params })
418d092a 142
453e83ea 143 if (areValidationErrors(req, res)) return
418d092a 144
453e83ea 145 if (!await doesVideoPlaylistExist(req.params.playlistId, res, fetchType)) return
418d092a 146
453e83ea 147 const videoPlaylist = res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
07b1a18a 148
453e83ea
C
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()
07b1a18a 152
76148b27
RK
153 return res.fail({
154 status: HttpStatusCode.NOT_FOUND_404,
155 message: 'Playlist not found'
156 })
453e83ea 157 }
07b1a18a 158
453e83ea
C
159 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
160 await authenticatePromiseIfNeeded(req, res)
418d092a 161
453e83ea 162 const user = res.locals.oauth ? res.locals.oauth.token.User : null
4d09cfba 163
453e83ea
C
164 if (
165 !user ||
166 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
167 ) {
76148b27
RK
168 return res.fail({
169 status: HttpStatusCode.FORBIDDEN_403,
170 message: 'Cannot get this private video playlist.'
171 })
453e83ea
C
172 }
173
174 return next()
418d092a
C
175 }
176
177 return next()
178 }
453e83ea
C
179 ]
180}
418d092a 181
c06af501
RK
182const 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
418d092a 194const videoPlaylistsAddVideoValidator = [
d4a8e7a6
C
195 isValidPlaylistIdParam('playlistId'),
196
418d092a 197 body('videoId')
d4a8e7a6 198 .customSanitizer(toCompleteUUID)
418d092a
C
199 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'),
200 body('startTimestamp')
201 .optional()
df0b219d 202 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
418d092a
C
203 body('stopTimestamp')
204 .optional()
df0b219d 205 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
418d092a
C
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
0f6acda1
C
212 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
213 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
418d092a 214
453e83ea 215 const videoPlaylist = getPlaylist(res)
418d092a 216
453e83ea 217 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
418d092a
C
218 return
219 }
220
221 return next()
222 }
223]
224
225const videoPlaylistsUpdateOrRemoveVideoValidator = [
d4a8e7a6 226 isValidPlaylistIdParam('playlistId'),
bfbd9128 227 param('playlistElementId')
d4a8e7a6 228 .customSanitizer(toCompleteUUID)
bfbd9128 229 .custom(isIdValid).withMessage('Should have an element id/uuid'),
418d092a
C
230 body('startTimestamp')
231 .optional()
df0b219d 232 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
418d092a
C
233 body('stopTimestamp')
234 .optional()
df0b219d 235 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
418d092a
C
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
0f6acda1 242 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
418d092a 243
453e83ea 244 const videoPlaylist = getPlaylist(res)
418d092a 245
bfbd9128 246 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
418d092a 247 if (!videoPlaylistElement) {
76148b27
RK
248 res.fail({
249 status: HttpStatusCode.NOT_FOUND_404,
250 message: 'Video playlist element not found'
251 })
418d092a
C
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
262const videoPlaylistElementAPGetValidator = [
d4a8e7a6 263 isValidPlaylistIdParam('playlistId'),
37190663
C
264 param('playlistElementId')
265 .custom(isIdValid).withMessage('Should have an playlist element id'),
418d092a
C
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
37190663
C
272 const playlistElementId = parseInt(req.params.playlistElementId + '', 10)
273 const playlistId = req.params.playlistId
274
275 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
418d092a 276 if (!videoPlaylistElement) {
76148b27
RK
277 res.fail({
278 status: HttpStatusCode.NOT_FOUND_404,
279 message: 'Video playlist element not found'
280 })
418d092a
C
281 return
282 }
283
284 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
76148b27
RK
285 return res.fail({
286 status: HttpStatusCode.FORBIDDEN_403,
287 message: 'Cannot get this private video playlist.'
288 })
418d092a
C
289 }
290
b5fecbf4 291 res.locals.videoPlaylistElementAP = videoPlaylistElement
418d092a
C
292
293 return next()
294 }
295]
296
297const videoPlaylistsReorderVideosValidator = [
d4a8e7a6 298 isValidPlaylistIdParam('playlistId'),
418d092a
C
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
0f6acda1 312 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
418d092a 313
453e83ea 314 const videoPlaylist = getPlaylist(res)
418d092a
C
315 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
316
07b1a18a
C
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) {
76148b27 323 res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
07b1a18a
C
324 return
325 }
326
327 if (reorderLength && reorderLength + startPosition > nextPosition) {
76148b27 328 res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
07b1a18a
C
329 return
330 }
331
418d092a
C
332 return next()
333 }
334]
335
df0b219d
C
336const 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
f0a39880
C
350const 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
418d092a
C
364// ---------------------------------------------------------------------------
365
366export {
367 videoPlaylistsAddValidator,
368 videoPlaylistsUpdateValidator,
369 videoPlaylistsDeleteValidator,
370 videoPlaylistsGetValidator,
c06af501 371 videoPlaylistsSearchValidator,
418d092a
C
372
373 videoPlaylistsAddVideoValidator,
374 videoPlaylistsUpdateOrRemoveVideoValidator,
375 videoPlaylistsReorderVideosValidator,
376
df0b219d
C
377 videoPlaylistElementAPGetValidator,
378
f0a39880
C
379 commonVideoPlaylistFiltersValidator,
380
381 doVideosInPlaylistExistValidator
418d092a
C
382}
383
384// ---------------------------------------------------------------------------
385
386function getCommonPlaylistEditAttributes () {
387 return [
388 body('thumbnailfile')
a1587156
C
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 ),
418d092a 394
418d092a
C
395 body('description')
396 .optional()
397 .customSanitizer(toValueOrNull)
398 .custom(isVideoPlaylistDescriptionValid).withMessage('Should have a valid description'),
399 body('privacy')
400 .optional()
c8861d5d 401 .customSanitizer(toIntOrNull)
418d092a
C
402 .custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
403 body('videoChannelId')
404 .optional()
c8861d5d 405 .customSanitizer(toIntOrNull)
ba5a8d89 406 ] as (ValidationChain | ExpressPromiseHandler)[]
418d092a
C
407}
408
453e83ea 409function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
418d092a 410 if (videoPlaylist.isOwned() === false) {
76148b27
RK
411 res.fail({
412 status: HttpStatusCode.FORBIDDEN_403,
413 message: 'Cannot manage video playlist of another server.'
414 })
418d092a
C
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) {
76148b27
RK
422 res.fail({
423 status: HttpStatusCode.FORBIDDEN_403,
424 message: 'Cannot manage video playlist of another user'
425 })
418d092a
C
426 return false
427 }
428
429 return true
430}
453e83ea
C
431
432function getPlaylist (res: express.Response) {
433 return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
434}