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 = (
151 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
152 authenticateInQuery = false
155 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
157 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
158 logger.debug('Checking videosGet parameters', { parameters: req.params })
160 if (areValidationErrors(req, res)) return
161 if (!await doesVideoExist(req.params.id, res, fetchType)) return
163 const video = getVideoWithAttributes(res)
164 const videoAll = video as MVideoFullLight
166 // Video private or blacklisted
167 if (videoAll.requiresAuth()) {
168 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
170 const user = res.locals.oauth ? res.locals.oauth.token.User : null
172 // Only the owner or a user that have blacklist rights can see the video
173 if (!user || !user.canGetVideo(videoAll)) {
174 return res.status(403)
175 .json({ error: 'Cannot get this private/internal 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')
196 const videosDownloadValidator = videosCustomGetValidator('all', true)
198 const videosRemoveValidator = [
199 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
201 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
202 logger.debug('Checking videosRemove parameters', { parameters: req.params })
204 if (areValidationErrors(req, res)) return
205 if (!await doesVideoExist(req.params.id, res)) return
207 // Check if the user who did the request is able to delete the video
208 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
214 const videosChangeOwnershipValidator = [
215 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
217 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
218 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
220 if (areValidationErrors(req, res)) return
221 if (!await doesVideoExist(req.params.videoId, res)) return
223 // Check if the user who did the request is able to change the ownership of the video
224 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
226 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
229 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
233 res.locals.nextOwner = nextOwner
239 const videosTerminateChangeOwnershipValidator = [
240 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
242 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
243 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
245 if (areValidationErrors(req, res)) return
246 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
248 // Check if the user who did the request is able to change the ownership of the video
249 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
251 const videoChangeOwnership = res.locals.videoChangeOwnership
253 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
255 .json({ error: 'Ownership already accepted or refused' })
263 const videosAcceptChangeOwnershipValidator = [
264 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
265 const body = req.body as VideoChangeOwnershipAccept
266 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
268 const user = res.locals.oauth.token.User
269 const videoChangeOwnership = res.locals.videoChangeOwnership
270 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getMaxQualityFile())
271 if (isAble === false) {
273 .json({ error: 'The user video quota is exceeded with this video.' })
282 function getCommonVideoEditAttributes () {
284 body('thumbnailfile')
285 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
286 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
287 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
290 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
291 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
292 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
297 .customSanitizer(toIntOrNull)
298 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
301 .customSanitizer(toIntOrNull)
302 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
305 .customSanitizer(toValueOrNull)
306 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
309 .customSanitizer(toBooleanOrNull)
310 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
311 body('waitTranscoding')
313 .customSanitizer(toBooleanOrNull)
314 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
317 .customSanitizer(toValueOrNull)
318 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
321 .customSanitizer(toValueOrNull)
322 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
325 .customSanitizer(toValueOrNull)
326 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
329 .customSanitizer(toValueOrNull)
330 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
331 body('commentsEnabled')
333 .customSanitizer(toBooleanOrNull)
334 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
335 body('downloadEnabled')
337 .customSanitizer(toBooleanOrNull)
338 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
339 body('originallyPublishedAt')
341 .customSanitizer(toValueOrNull)
342 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
343 body('scheduleUpdate')
345 .customSanitizer(toValueOrNull),
346 body('scheduleUpdate.updateAt')
348 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
349 body('scheduleUpdate.privacy')
351 .customSanitizer(toIntOrNull)
352 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
353 ] as (ValidationChain | express.Handler)[]
356 const commonVideosFiltersValidator = [
357 query('categoryOneOf')
359 .customSanitizer(toArray)
360 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
361 query('licenceOneOf')
363 .customSanitizer(toArray)
364 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
365 query('languageOneOf')
367 .customSanitizer(toArray)
368 .custom(isStringArray).withMessage('Should have a valid one of language array'),
371 .customSanitizer(toArray)
372 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
375 .customSanitizer(toArray)
376 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
379 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
382 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
385 .customSanitizer(toBooleanOrNull)
386 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
388 (req: express.Request, res: express.Response, next: express.NextFunction) => {
389 logger.debug('Checking commons video filters query', { parameters: req.query })
391 if (areValidationErrors(req, res)) return
393 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
394 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
396 .json({ error: 'You are not allowed to see all local videos.' })
405 // ---------------------------------------------------------------------------
409 videosUpdateValidator,
411 videosDownloadValidator,
412 checkVideoFollowConstraints,
413 videosCustomGetValidator,
414 videosRemoveValidator,
416 videosChangeOwnershipValidator,
417 videosTerminateChangeOwnershipValidator,
418 videosAcceptChangeOwnershipValidator,
420 getCommonVideoEditAttributes,
422 commonVideosFiltersValidator
425 // ---------------------------------------------------------------------------
427 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
428 if (req.body.scheduleUpdate) {
429 if (!req.body.scheduleUpdate.updateAt) {
430 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
433 .json({ error: 'Schedule update at is mandatory.' })
442 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
443 // Check we accept this video
444 const acceptParameters = {
447 user: res.locals.oauth.token.User
449 const acceptedResult = await Hooks.wrapFun(
450 isLocalVideoAccepted,
452 'filter:api.video.upload.accept.result'
455 if (!acceptedResult || acceptedResult.accepted !== true) {
456 logger.info('Refused local video.', { acceptedResult, acceptParameters })
458 .json({ error: acceptedResult.errorMessage || 'Refused local video' })