1 import * as express from 'express'
2 import { body, param, query, ValidationChain } from 'express-validator'
3 import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
14 } from '../../../helpers/custom-validators/misc'
16 isScheduleVideoUpdatePrivacyValid,
18 isVideoDescriptionValid,
25 isVideoOriginallyPublishedAtValid,
29 } from '../../../helpers/custom-validators/videos'
30 import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
31 import { logger } from '../../../helpers/logger'
32 import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
33 import { authenticatePromiseIfNeeded } from '../../oauth'
34 import { areValidationErrors } from '../utils'
35 import { cleanUpReqFiles } from '../../../helpers/express-utils'
36 import { VideoModel } from '../../../models/video/video'
37 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
38 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
39 import { AccountModel } from '../../../models/account/account'
40 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
41 import { getServerActor } from '../../../helpers/utils'
42 import { CONFIG } from '../../../initializers/config'
43 import { isLocalVideoAccepted } from '../../../lib/moderation'
44 import { Hooks } from '../../../lib/plugins/hooks'
45 import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '../../../helpers/middlewares'
46 import { MVideoFullLight } from '@server/typings/models'
47 import { getVideoWithAttributes } from '../../../helpers/video'
49 const videosAddValidator = getCommonVideoEditAttributes().concat([
51 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
52 'This file is not supported or too large. Please, make sure it is of the following type: '
53 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
55 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
57 .customSanitizer(toIntOrNull)
58 .custom(isIdValid).withMessage('Should have correct video channel id'),
60 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
61 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
63 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
64 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
66 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
67 const user = res.locals.oauth.token.User
69 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
71 if (await user.isAbleToUploadVideo(videoFile) === false) {
73 .json({ error: 'The user video quota is exceeded with this video.' })
75 return cleanUpReqFiles(req)
81 duration = await getDurationFromVideoFile(videoFile.path)
83 logger.error('Invalid input file in videosAddValidator.', { err })
85 .json({ error: 'Invalid input file.' })
87 return cleanUpReqFiles(req)
90 videoFile.duration = duration
92 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
98 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
99 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
102 .custom(isVideoNameValid).withMessage('Should have a valid name'),
105 .customSanitizer(toIntOrNull)
106 .custom(isIdValid).withMessage('Should have correct video channel id'),
108 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
109 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
111 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
112 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
113 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
115 // Check if the user who did the request is able to update the video
116 const user = res.locals.oauth.token.User
117 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
119 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
125 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
126 const video = getVideoWithAttributes(res)
128 // Anybody can watch local videos
129 if (video.isOwned() === true) return next()
132 if (res.locals.oauth) {
133 // Users can search or watch remote videos
134 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
137 // Anybody can search or watch remote videos
138 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
140 // Check our instance follows an actor that shared this video
141 const serverActor = await getServerActor()
142 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
144 return res.status(403)
146 error: 'Cannot get this video regarding follow constraints.'
150 const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-with-rights') => {
152 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
154 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
155 logger.debug('Checking videosGet parameters', { parameters: req.params })
157 if (areValidationErrors(req, res)) return
158 if (!await doesVideoExist(req.params.id, res, fetchType)) return
160 const video = getVideoWithAttributes(res)
161 const videoAll = video as MVideoFullLight
163 // Video private or blacklisted
164 if (video.privacy === VideoPrivacy.PRIVATE || videoAll.VideoBlacklist) {
165 await authenticatePromiseIfNeeded(req, res)
167 const user = res.locals.oauth ? res.locals.oauth.token.User : null
169 // Only the owner or a user that have blacklist rights can see the video
172 (videoAll.VideoChannel && videoAll.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
174 return res.status(403)
175 .json({ error: 'Cannot get this private or blacklisted video.' })
181 // Video is public, anyone can access it
182 if (video.privacy === VideoPrivacy.PUBLIC) return next()
184 // Video is unlisted, check we used the uuid to fetch it
185 if (video.privacy === VideoPrivacy.UNLISTED) {
186 if (isUUIDValid(req.params.id)) return next()
188 // Don't leak this unlisted video
189 return res.status(404).end()
195 const videosGetValidator = videosCustomGetValidator('all')
197 const videosRemoveValidator = [
198 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
200 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
201 logger.debug('Checking videosRemove parameters', { parameters: req.params })
203 if (areValidationErrors(req, res)) return
204 if (!await doesVideoExist(req.params.id, res)) return
206 // Check if the user who did the request is able to delete the video
207 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
213 const videosChangeOwnershipValidator = [
214 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
216 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
217 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
219 if (areValidationErrors(req, res)) return
220 if (!await doesVideoExist(req.params.videoId, res)) return
222 // Check if the user who did the request is able to change the ownership of the video
223 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
225 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
228 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
232 res.locals.nextOwner = nextOwner
238 const videosTerminateChangeOwnershipValidator = [
239 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
241 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
242 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
244 if (areValidationErrors(req, res)) return
245 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
247 // Check if the user who did the request is able to change the ownership of the video
248 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
252 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
253 const videoChangeOwnership = res.locals.videoChangeOwnership
255 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
259 .json({ error: 'Ownership already accepted or refused' })
266 const videosAcceptChangeOwnershipValidator = [
267 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
268 const body = req.body as VideoChangeOwnershipAccept
269 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
271 const user = res.locals.oauth.token.User
272 const videoChangeOwnership = res.locals.videoChangeOwnership
273 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
274 if (isAble === false) {
276 .json({ error: 'The user video quota is exceeded with this video.' })
285 function getCommonVideoEditAttributes () {
287 body('thumbnailfile')
288 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
289 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
290 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
293 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
294 'This preview file is not supported or too large. Please, make sure it is of the following type: '
295 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
300 .customSanitizer(toIntOrNull)
301 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
304 .customSanitizer(toIntOrNull)
305 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
308 .customSanitizer(toValueOrNull)
309 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
312 .customSanitizer(toBooleanOrNull)
313 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
314 body('waitTranscoding')
316 .customSanitizer(toBooleanOrNull)
317 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
320 .customSanitizer(toValueOrNull)
321 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
324 .customSanitizer(toValueOrNull)
325 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
328 .customSanitizer(toValueOrNull)
329 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
332 .customSanitizer(toValueOrNull)
333 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
334 body('commentsEnabled')
336 .customSanitizer(toBooleanOrNull)
337 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
338 body('downloadEnabled')
340 .customSanitizer(toBooleanOrNull)
341 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
342 body('originallyPublishedAt')
344 .customSanitizer(toValueOrNull)
345 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
346 body('scheduleUpdate')
348 .customSanitizer(toValueOrNull),
349 body('scheduleUpdate.updateAt')
351 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
352 body('scheduleUpdate.privacy')
354 .customSanitizer(toIntOrNull)
355 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
356 ] as (ValidationChain | express.Handler)[]
359 const commonVideosFiltersValidator = [
360 query('categoryOneOf')
362 .customSanitizer(toArray)
363 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
364 query('licenceOneOf')
366 .customSanitizer(toArray)
367 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
368 query('languageOneOf')
370 .customSanitizer(toArray)
371 .custom(isStringArray).withMessage('Should have a valid one of language array'),
374 .customSanitizer(toArray)
375 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
378 .customSanitizer(toArray)
379 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
382 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
385 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
387 (req: express.Request, res: express.Response, next: express.NextFunction) => {
388 logger.debug('Checking commons video filters query', { parameters: req.query })
390 if (areValidationErrors(req, res)) return
392 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
393 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
395 .json({ error: 'You are not allowed to see all local videos.' })
404 // ---------------------------------------------------------------------------
408 videosUpdateValidator,
410 checkVideoFollowConstraints,
411 videosCustomGetValidator,
412 videosRemoveValidator,
414 videosChangeOwnershipValidator,
415 videosTerminateChangeOwnershipValidator,
416 videosAcceptChangeOwnershipValidator,
418 getCommonVideoEditAttributes,
420 commonVideosFiltersValidator
423 // ---------------------------------------------------------------------------
425 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
426 if (req.body.scheduleUpdate) {
427 if (!req.body.scheduleUpdate.updateAt) {
428 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
431 .json({ error: 'Schedule update at is mandatory.' })
440 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
441 // Check we accept this video
442 const acceptParameters = {
445 user: res.locals.oauth.token.User
447 const acceptedResult = await Hooks.wrapFun(
448 isLocalVideoAccepted,
450 'filter:api.video.upload.accept.result'
453 if (!acceptedResult || acceptedResult.accepted !== true) {
454 logger.info('Refused local video.', { acceptedResult, acceptParameters })
456 .json({ error: acceptedResult.errorMessage || 'Refused local video' })