Check activities host
[oweals/peertube.git] / server / middlewares / validators / videos / videos.ts
1 import * as express from 'express'
2 import 'express-validator'
3 import { body, param, query, ValidationChain } from 'express-validator/check'
4 import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
5 import {
6   isBooleanValid,
7   isDateValid,
8   isIdOrUUIDValid,
9   isIdValid,
10   isUUIDValid,
11   toArray,
12   toIntOrNull,
13   toValueOrNull
14 } from '../../../helpers/custom-validators/misc'
15 import {
16   checkUserCanManageVideo,
17   isScheduleVideoUpdatePrivacyValid,
18   isVideoCategoryValid,
19   isVideoChannelOfAccountExist,
20   isVideoDescriptionValid,
21   isVideoExist,
22   isVideoFile,
23   isVideoFilterValid,
24   isVideoImage,
25   isVideoLanguageValid,
26   isVideoLicenceValid,
27   isVideoNameValid,
28   isVideoPrivacyValid,
29   isVideoSupportValid,
30   isVideoTagsValid
31 } from '../../../helpers/custom-validators/videos'
32 import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
33 import { logger } from '../../../helpers/logger'
34 import { CONSTRAINTS_FIELDS } from '../../../initializers'
35 import { authenticate } from '../../oauth'
36 import { areValidationErrors } from '../utils'
37 import { cleanUpReqFiles } from '../../../helpers/express-utils'
38 import { VideoModel } from '../../../models/video/video'
39 import { UserModel } from '../../../models/account/user'
40 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
41 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
42 import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
43 import { AccountModel } from '../../../models/account/account'
44 import { VideoFetchType } from '../../../helpers/video'
45 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
46
47 const videosAddValidator = getCommonVideoAttributes().concat([
48   body('videofile')
49     .custom((value, { req }) => isVideoFile(req.files)).withMessage(
50       'This file is not supported or too large. Please, make sure it is of the following type: '
51       + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
52     ),
53   body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
54   body('channelId')
55     .toInt()
56     .custom(isIdValid).withMessage('Should have correct video channel id'),
57
58   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
59     logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
60
61     if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
62     if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
63
64     const videoFile: Express.Multer.File = req.files['videofile'][0]
65     const user = res.locals.oauth.token.User
66
67     if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
68
69     const isAble = await user.isAbleToUploadVideo(videoFile)
70     if (isAble === false) {
71       res.status(403)
72          .json({ error: 'The user video quota is exceeded with this video.' })
73
74       return cleanUpReqFiles(req)
75     }
76
77     let duration: number
78
79     try {
80       duration = await getDurationFromVideoFile(videoFile.path)
81     } catch (err) {
82       logger.error('Invalid input file in videosAddValidator.', { err })
83       res.status(400)
84          .json({ error: 'Invalid input file.' })
85
86       return cleanUpReqFiles(req)
87     }
88
89     videoFile['duration'] = duration
90
91     return next()
92   }
93 ])
94
95 const videosUpdateValidator = getCommonVideoAttributes().concat([
96   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
97   body('name')
98     .optional()
99     .custom(isVideoNameValid).withMessage('Should have a valid name'),
100   body('channelId')
101     .optional()
102     .toInt()
103     .custom(isIdValid).withMessage('Should have correct video channel id'),
104
105   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
106     logger.debug('Checking videosUpdate parameters', { parameters: req.body })
107
108     if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
109     if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
110     if (!await isVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
111
112     const video = res.locals.video
113
114     // Check if the user who did the request is able to update the video
115     const user = res.locals.oauth.token.User
116     if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
117
118     if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
119       cleanUpReqFiles(req)
120       return res.status(409)
121         .json({ error: 'Cannot set "private" a video that was not private.' })
122     }
123
124     if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
125
126     return next()
127   }
128 ])
129
130 const videosCustomGetValidator = (fetchType: VideoFetchType) => {
131   return [
132     param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
133
134     async (req: express.Request, res: express.Response, next: express.NextFunction) => {
135       logger.debug('Checking videosGet parameters', { parameters: req.params })
136
137       if (areValidationErrors(req, res)) return
138       if (!await isVideoExist(req.params.id, res, fetchType)) return
139
140       const video: VideoModel = res.locals.video
141
142       // Video private or blacklisted
143       if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
144         return authenticate(req, res, () => {
145           const user: UserModel = res.locals.oauth.token.User
146
147           // Only the owner or a user that have blacklist rights can see the video
148           if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
149             return res.status(403)
150                       .json({ error: 'Cannot get this private or blacklisted video.' })
151           }
152
153           return next()
154         })
155       }
156
157       // Video is public, anyone can access it
158       if (video.privacy === VideoPrivacy.PUBLIC) return next()
159
160       // Video is unlisted, check we used the uuid to fetch it
161       if (video.privacy === VideoPrivacy.UNLISTED) {
162         if (isUUIDValid(req.params.id)) return next()
163
164         // Don't leak this unlisted video
165         return res.status(404).end()
166       }
167     }
168   ]
169 }
170
171 const videosGetValidator = videosCustomGetValidator('all')
172
173 const videosRemoveValidator = [
174   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
175
176   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
177     logger.debug('Checking videosRemove parameters', { parameters: req.params })
178
179     if (areValidationErrors(req, res)) return
180     if (!await isVideoExist(req.params.id, res)) return
181
182     // Check if the user who did the request is able to delete the video
183     if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
184
185     return next()
186   }
187 ]
188
189 const videosChangeOwnershipValidator = [
190   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
191
192   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
193     logger.debug('Checking changeOwnership parameters', { parameters: req.params })
194
195     if (areValidationErrors(req, res)) return
196     if (!await isVideoExist(req.params.videoId, res)) return
197
198     // Check if the user who did the request is able to change the ownership of the video
199     if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
200
201     const nextOwner = await AccountModel.loadLocalByName(req.body.username)
202     if (!nextOwner) {
203       res.status(400)
204         .json({ error: 'Changing video ownership to a remote account is not supported yet' })
205
206       return
207     }
208     res.locals.nextOwner = nextOwner
209
210     return next()
211   }
212 ]
213
214 const videosTerminateChangeOwnershipValidator = [
215   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
216
217   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
218     logger.debug('Checking changeOwnership parameters', { parameters: req.params })
219
220     if (areValidationErrors(req, res)) return
221     if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
222
223     // Check if the user who did the request is able to change the ownership of the video
224     if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
225
226     return next()
227   },
228   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
229     const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
230
231     if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
232       return next()
233     } else {
234       res.status(403)
235         .json({ error: 'Ownership already accepted or refused' })
236
237       return
238     }
239   }
240 ]
241
242 const videosAcceptChangeOwnershipValidator = [
243   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
244     const body = req.body as VideoChangeOwnershipAccept
245     if (!await isVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
246
247     const user = res.locals.oauth.token.User
248     const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
249     const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
250     if (isAble === false) {
251       res.status(403)
252         .json({ error: 'The user video quota is exceeded with this video.' })
253
254       return
255     }
256
257     return next()
258   }
259 ]
260
261 function getCommonVideoAttributes () {
262   return [
263     body('thumbnailfile')
264       .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
265       'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
266       + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
267     ),
268     body('previewfile')
269       .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
270       'This preview file is not supported or too large. Please, make sure it is of the following type: '
271       + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
272     ),
273
274     body('category')
275       .optional()
276       .customSanitizer(toIntOrNull)
277       .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
278     body('licence')
279       .optional()
280       .customSanitizer(toIntOrNull)
281       .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
282     body('language')
283       .optional()
284       .customSanitizer(toValueOrNull)
285       .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
286     body('nsfw')
287       .optional()
288       .toBoolean()
289       .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
290     body('waitTranscoding')
291       .optional()
292       .toBoolean()
293       .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
294     body('privacy')
295       .optional()
296       .toInt()
297       .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
298     body('description')
299       .optional()
300       .customSanitizer(toValueOrNull)
301       .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
302     body('support')
303       .optional()
304       .customSanitizer(toValueOrNull)
305       .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
306     body('tags')
307       .optional()
308       .customSanitizer(toValueOrNull)
309       .custom(isVideoTagsValid).withMessage('Should have correct tags'),
310     body('commentsEnabled')
311       .optional()
312       .toBoolean()
313       .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
314
315     body('scheduleUpdate')
316       .optional()
317       .customSanitizer(toValueOrNull),
318     body('scheduleUpdate.updateAt')
319       .optional()
320       .custom(isDateValid).withMessage('Should have a valid schedule update date'),
321     body('scheduleUpdate.privacy')
322       .optional()
323       .toInt()
324       .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
325   ] as (ValidationChain | express.Handler)[]
326 }
327
328 const commonVideosFiltersValidator = [
329   query('categoryOneOf')
330     .optional()
331     .customSanitizer(toArray)
332     .custom(isNumberArray).withMessage('Should have a valid one of category array'),
333   query('licenceOneOf')
334     .optional()
335     .customSanitizer(toArray)
336     .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
337   query('languageOneOf')
338     .optional()
339     .customSanitizer(toArray)
340     .custom(isStringArray).withMessage('Should have a valid one of language array'),
341   query('tagsOneOf')
342     .optional()
343     .customSanitizer(toArray)
344     .custom(isStringArray).withMessage('Should have a valid one of tags array'),
345   query('tagsAllOf')
346     .optional()
347     .customSanitizer(toArray)
348     .custom(isStringArray).withMessage('Should have a valid all of tags array'),
349   query('nsfw')
350     .optional()
351     .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
352   query('filter')
353     .optional()
354     .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
355
356   (req: express.Request, res: express.Response, next: express.NextFunction) => {
357     logger.debug('Checking commons video filters query', { parameters: req.query })
358
359     if (areValidationErrors(req, res)) return
360
361     const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
362     if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
363       res.status(401)
364          .json({ error: 'You are not allowed to see all local videos.' })
365
366       return
367     }
368
369     return next()
370   }
371 ]
372
373 // ---------------------------------------------------------------------------
374
375 export {
376   videosAddValidator,
377   videosUpdateValidator,
378   videosGetValidator,
379   videosCustomGetValidator,
380   videosRemoveValidator,
381
382   videosChangeOwnershipValidator,
383   videosTerminateChangeOwnershipValidator,
384   videosAcceptChangeOwnershipValidator,
385
386   getCommonVideoAttributes,
387
388   commonVideosFiltersValidator
389 }
390
391 // ---------------------------------------------------------------------------
392
393 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
394   if (req.body.scheduleUpdate) {
395     if (!req.body.scheduleUpdate.updateAt) {
396       res.status(400)
397          .json({ error: 'Schedule update at is mandatory.' })
398
399       return true
400     }
401   }
402
403   return false
404 }