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', authenticateInQuery = false) => {
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 (videoAll.requiresAuth()) {
165 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
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
170 if (!user || !user.canGetVideo(videoAll)) {
171 return res.status(403)
172 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
178 // Video is public, anyone can access it
179 if (video.privacy === VideoPrivacy.PUBLIC) return next()
181 // Video is unlisted, check we used the uuid to fetch it
182 if (video.privacy === VideoPrivacy.UNLISTED) {
183 if (isUUIDValid(req.params.id)) return next()
185 // Don't leak this unlisted video
186 return res.status(404).end()
192 const videosGetValidator = videosCustomGetValidator('all')
193 const videosDownloadValidator = videosCustomGetValidator('all', true)
195 const videosRemoveValidator = [
196 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
198 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
199 logger.debug('Checking videosRemove parameters', { parameters: req.params })
201 if (areValidationErrors(req, res)) return
202 if (!await doesVideoExist(req.params.id, res)) return
204 // Check if the user who did the request is able to delete the video
205 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
211 const videosChangeOwnershipValidator = [
212 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
214 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
215 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
217 if (areValidationErrors(req, res)) return
218 if (!await doesVideoExist(req.params.videoId, res)) return
220 // Check if the user who did the request is able to change the ownership of the video
221 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
223 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
226 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
230 res.locals.nextOwner = nextOwner
236 const videosTerminateChangeOwnershipValidator = [
237 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
239 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
240 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
242 if (areValidationErrors(req, res)) return
243 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
245 // Check if the user who did the request is able to change the ownership of the video
246 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
250 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
251 const videoChangeOwnership = res.locals.videoChangeOwnership
253 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
257 .json({ error: 'Ownership already accepted or refused' })
264 const videosAcceptChangeOwnershipValidator = [
265 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
266 const body = req.body as VideoChangeOwnershipAccept
267 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
269 const user = res.locals.oauth.token.User
270 const videoChangeOwnership = res.locals.videoChangeOwnership
271 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getMaxQualityFile())
272 if (isAble === false) {
274 .json({ error: 'The user video quota is exceeded with this video.' })
283 function getCommonVideoEditAttributes () {
285 body('thumbnailfile')
286 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
287 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
288 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
291 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
292 'This preview file is not supported or too large. Please, make sure it is of the following type: '
293 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
298 .customSanitizer(toIntOrNull)
299 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
302 .customSanitizer(toIntOrNull)
303 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
306 .customSanitizer(toValueOrNull)
307 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
310 .customSanitizer(toBooleanOrNull)
311 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
312 body('waitTranscoding')
314 .customSanitizer(toBooleanOrNull)
315 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
318 .customSanitizer(toValueOrNull)
319 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
322 .customSanitizer(toValueOrNull)
323 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
326 .customSanitizer(toValueOrNull)
327 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
330 .customSanitizer(toValueOrNull)
331 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
332 body('commentsEnabled')
334 .customSanitizer(toBooleanOrNull)
335 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
336 body('downloadEnabled')
338 .customSanitizer(toBooleanOrNull)
339 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
340 body('originallyPublishedAt')
342 .customSanitizer(toValueOrNull)
343 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
344 body('scheduleUpdate')
346 .customSanitizer(toValueOrNull),
347 body('scheduleUpdate.updateAt')
349 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
350 body('scheduleUpdate.privacy')
352 .customSanitizer(toIntOrNull)
353 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
354 ] as (ValidationChain | express.Handler)[]
357 const commonVideosFiltersValidator = [
358 query('categoryOneOf')
360 .customSanitizer(toArray)
361 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
362 query('licenceOneOf')
364 .customSanitizer(toArray)
365 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
366 query('languageOneOf')
368 .customSanitizer(toArray)
369 .custom(isStringArray).withMessage('Should have a valid one of language array'),
372 .customSanitizer(toArray)
373 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
376 .customSanitizer(toArray)
377 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
380 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
383 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
386 .customSanitizer(toBooleanOrNull)
387 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
389 (req: express.Request, res: express.Response, next: express.NextFunction) => {
390 logger.debug('Checking commons video filters query', { parameters: req.query })
392 if (areValidationErrors(req, res)) return
394 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
395 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
397 .json({ error: 'You are not allowed to see all local videos.' })
406 // ---------------------------------------------------------------------------
410 videosUpdateValidator,
412 videosDownloadValidator,
413 checkVideoFollowConstraints,
414 videosCustomGetValidator,
415 videosRemoveValidator,
417 videosChangeOwnershipValidator,
418 videosTerminateChangeOwnershipValidator,
419 videosAcceptChangeOwnershipValidator,
421 getCommonVideoEditAttributes,
423 commonVideosFiltersValidator
426 // ---------------------------------------------------------------------------
428 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
429 if (req.body.scheduleUpdate) {
430 if (!req.body.scheduleUpdate.updateAt) {
431 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
434 .json({ error: 'Schedule update at is mandatory.' })
443 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
444 // Check we accept this video
445 const acceptParameters = {
448 user: res.locals.oauth.token.User
450 const acceptedResult = await Hooks.wrapFun(
451 isLocalVideoAccepted,
453 'filter:api.video.upload.accept.result'
456 if (!acceptedResult || acceptedResult.accepted !== true) {
457 logger.info('Refused local video.', { acceptedResult, acceptParameters })
459 .json({ error: acceptedResult.errorMessage || 'Refused local video' })