1 import * as express from 'express'
2 import { body, param, query, ValidationChain } from 'express-validator'
3 import { isAbleToUploadVideo } from '@server/lib/user'
4 import { getServerActor } from '@server/models/application/application'
5 import { MVideoFullLight } from '@server/types/models'
6 import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
7 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
20 } from '../../../helpers/custom-validators/misc'
21 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
22 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
24 isScheduleVideoUpdatePrivacyValid,
26 isVideoDescriptionValid,
27 isVideoFileMimeTypeValid,
34 isVideoOriginallyPublishedAtValid,
38 } from '../../../helpers/custom-validators/videos'
39 import { cleanUpReqFiles } from '../../../helpers/express-utils'
40 import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils'
41 import { logger } from '../../../helpers/logger'
43 checkUserCanManageVideo,
44 doesVideoChannelOfAccountExist,
46 doesVideoFileOfVideoExist
47 } from '../../../helpers/middlewares'
48 import { getVideoWithAttributes } from '../../../helpers/video'
49 import { CONFIG } from '../../../initializers/config'
50 import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
51 import { isLocalVideoAccepted } from '../../../lib/moderation'
52 import { Hooks } from '../../../lib/plugins/hooks'
53 import { AccountModel } from '../../../models/account/account'
54 import { VideoModel } from '../../../models/video/video'
55 import { authenticatePromiseIfNeeded } from '../../oauth'
56 import { areValidationErrors } from '../utils'
57 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
59 const videosAddValidator = getCommonVideoEditAttributes().concat([
61 .custom((value, { req }) => isFileFieldValid(req.files, 'videofile'))
62 .withMessage('Should have a file'),
64 .custom(isVideoNameValid)
65 .withMessage('Should have a valid name'),
67 .customSanitizer(toIntOrNull)
68 .custom(isIdValid).withMessage('Should have correct video channel id'),
70 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
71 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
73 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
74 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
76 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
77 const user = res.locals.oauth.token.User
79 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
81 if (!isVideoFileMimeTypeValid(req.files)) {
82 res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415)
84 error: 'This file is not supported. Please, make sure it is of the following type: ' +
85 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
88 return cleanUpReqFiles(req)
91 if (!isVideoFileSizeValid(videoFile.size.toString())) {
92 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
94 error: 'This file is too large.'
97 return cleanUpReqFiles(req)
100 if (await isAbleToUploadVideo(user.id, videoFile.size) === false) {
101 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
102 .json({ error: 'The user video quota is exceeded with this video.' })
104 return cleanUpReqFiles(req)
110 duration = await getDurationFromVideoFile(videoFile.path)
112 logger.error('Invalid input file in videosAddValidator.', { err })
113 res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
114 .json({ error: 'Video file unreadable.' })
116 return cleanUpReqFiles(req)
119 videoFile.duration = duration
121 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
127 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
128 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
131 .custom(isVideoNameValid).withMessage('Should have a valid name'),
134 .customSanitizer(toIntOrNull)
135 .custom(isIdValid).withMessage('Should have correct video channel id'),
137 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
138 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
140 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
141 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
142 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
144 // Check if the user who did the request is able to update the video
145 const user = res.locals.oauth.token.User
146 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
148 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
154 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
155 const video = getVideoWithAttributes(res)
157 // Anybody can watch local videos
158 if (video.isOwned() === true) return next()
161 if (res.locals.oauth) {
162 // Users can search or watch remote videos
163 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
166 // Anybody can search or watch remote videos
167 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
169 // Check our instance follows an actor that shared this video
170 const serverActor = await getServerActor()
171 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
173 return res.status(HttpStatusCode.FORBIDDEN_403)
175 errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
176 error: 'Cannot get this video regarding follow constraints.',
181 const videosCustomGetValidator = (
182 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
183 authenticateInQuery = false
186 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
188 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
189 logger.debug('Checking videosGet parameters', { parameters: req.params })
191 if (areValidationErrors(req, res)) return
192 if (!await doesVideoExist(req.params.id, res, fetchType)) return
194 // Controllers does not need to check video rights
195 if (fetchType === 'only-immutable-attributes') return next()
197 const video = getVideoWithAttributes(res)
198 const videoAll = video as MVideoFullLight
200 // Video private or blacklisted
201 if (videoAll.requiresAuth()) {
202 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
204 const user = res.locals.oauth ? res.locals.oauth.token.User : null
206 // Only the owner or a user that have blacklist rights can see the video
207 if (!user || !user.canGetVideo(videoAll)) {
208 return res.status(HttpStatusCode.FORBIDDEN_403)
209 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
215 // Video is public, anyone can access it
216 if (video.privacy === VideoPrivacy.PUBLIC) return next()
218 // Video is unlisted, check we used the uuid to fetch it
219 if (video.privacy === VideoPrivacy.UNLISTED) {
220 if (isUUIDValid(req.params.id)) return next()
222 // Don't leak this unlisted video
223 return res.status(HttpStatusCode.NOT_FOUND_404).end()
229 const videosGetValidator = videosCustomGetValidator('all')
230 const videosDownloadValidator = videosCustomGetValidator('all', true)
232 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
233 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
234 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
236 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
237 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
239 if (areValidationErrors(req, res)) return
240 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
246 const videosRemoveValidator = [
247 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
249 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
250 logger.debug('Checking videosRemove parameters', { parameters: req.params })
252 if (areValidationErrors(req, res)) return
253 if (!await doesVideoExist(req.params.id, res)) return
255 // Check if the user who did the request is able to delete the video
256 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
262 const videosChangeOwnershipValidator = [
263 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
265 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
266 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
268 if (areValidationErrors(req, res)) return
269 if (!await doesVideoExist(req.params.videoId, res)) return
271 // Check if the user who did the request is able to change the ownership of the video
272 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
274 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
276 res.status(HttpStatusCode.BAD_REQUEST_400)
277 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
281 res.locals.nextOwner = nextOwner
287 const videosTerminateChangeOwnershipValidator = [
288 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
290 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
291 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
293 if (areValidationErrors(req, res)) return
294 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
296 // Check if the user who did the request is able to change the ownership of the video
297 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
299 const videoChangeOwnership = res.locals.videoChangeOwnership
301 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
302 res.status(HttpStatusCode.FORBIDDEN_403)
303 .json({ error: 'Ownership already accepted or refused' })
311 const videosAcceptChangeOwnershipValidator = [
312 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
313 const body = req.body as VideoChangeOwnershipAccept
314 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
316 const user = res.locals.oauth.token.User
317 const videoChangeOwnership = res.locals.videoChangeOwnership
318 const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size)
319 if (isAble === false) {
320 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
321 .json({ error: 'The user video quota is exceeded with this video.' })
330 const videosOverviewValidator = [
333 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
334 .withMessage('Should have a valid pagination'),
336 (req: express.Request, res: express.Response, next: express.NextFunction) => {
337 if (areValidationErrors(req, res)) return
343 function getCommonVideoEditAttributes () {
345 body('thumbnailfile')
346 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
347 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
348 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
351 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
352 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
353 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
358 .customSanitizer(toIntOrNull)
359 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
362 .customSanitizer(toIntOrNull)
363 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
366 .customSanitizer(toValueOrNull)
367 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
370 .customSanitizer(toBooleanOrNull)
371 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
372 body('waitTranscoding')
374 .customSanitizer(toBooleanOrNull)
375 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
378 .customSanitizer(toValueOrNull)
379 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
382 .customSanitizer(toValueOrNull)
383 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
386 .customSanitizer(toValueOrNull)
387 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
390 .customSanitizer(toValueOrNull)
391 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
392 body('commentsEnabled')
394 .customSanitizer(toBooleanOrNull)
395 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
396 body('downloadEnabled')
398 .customSanitizer(toBooleanOrNull)
399 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
400 body('originallyPublishedAt')
402 .customSanitizer(toValueOrNull)
403 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
404 body('scheduleUpdate')
406 .customSanitizer(toValueOrNull),
407 body('scheduleUpdate.updateAt')
409 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
410 body('scheduleUpdate.privacy')
412 .customSanitizer(toIntOrNull)
413 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
414 ] as (ValidationChain | express.Handler)[]
417 const commonVideosFiltersValidator = [
418 query('categoryOneOf')
420 .customSanitizer(toArray)
421 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
422 query('licenceOneOf')
424 .customSanitizer(toArray)
425 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
426 query('languageOneOf')
428 .customSanitizer(toArray)
429 .custom(isStringArray).withMessage('Should have a valid one of language array'),
432 .customSanitizer(toArray)
433 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
436 .customSanitizer(toArray)
437 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
440 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
443 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
446 .customSanitizer(toBooleanOrNull)
447 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
450 .custom(exists).withMessage('Should have a valid search'),
452 (req: express.Request, res: express.Response, next: express.NextFunction) => {
453 logger.debug('Checking commons video filters query', { parameters: req.query })
455 if (areValidationErrors(req, res)) return
457 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
459 (req.query.filter === 'all-local' || req.query.filter === 'all') &&
460 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
462 res.status(HttpStatusCode.UNAUTHORIZED_401)
463 .json({ error: 'You are not allowed to see all local videos.' })
472 // ---------------------------------------------------------------------------
476 videosUpdateValidator,
478 videoFileMetadataGetValidator,
479 videosDownloadValidator,
480 checkVideoFollowConstraints,
481 videosCustomGetValidator,
482 videosRemoveValidator,
484 videosChangeOwnershipValidator,
485 videosTerminateChangeOwnershipValidator,
486 videosAcceptChangeOwnershipValidator,
488 getCommonVideoEditAttributes,
490 commonVideosFiltersValidator,
492 videosOverviewValidator
495 // ---------------------------------------------------------------------------
497 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
498 if (req.body.scheduleUpdate) {
499 if (!req.body.scheduleUpdate.updateAt) {
500 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
502 res.status(HttpStatusCode.BAD_REQUEST_400)
503 .json({ error: 'Schedule update at is mandatory.' })
512 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
513 // Check we accept this video
514 const acceptParameters = {
517 user: res.locals.oauth.token.User
519 const acceptedResult = await Hooks.wrapFun(
520 isLocalVideoAccepted,
522 'filter:api.video.upload.accept.result'
525 if (!acceptedResult || acceptedResult.accepted !== true) {
526 logger.info('Refused local video.', { acceptedResult, acceptParameters })
527 res.status(HttpStatusCode.FORBIDDEN_403)
528 .json({ error: acceptedResult.errorMessage || 'Refused local video' })