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'
19 } from '../../../helpers/custom-validators/misc'
20 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
21 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
23 isScheduleVideoUpdatePrivacyValid,
25 isVideoDescriptionValid,
26 isVideoFileMimeTypeValid,
33 isVideoOriginallyPublishedAtValid,
37 } from '../../../helpers/custom-validators/videos'
38 import { cleanUpReqFiles } from '../../../helpers/express-utils'
39 import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils'
40 import { logger } from '../../../helpers/logger'
42 checkUserCanManageVideo,
43 doesVideoChannelOfAccountExist,
45 doesVideoFileOfVideoExist
46 } from '../../../helpers/middlewares'
47 import { getVideoWithAttributes } from '../../../helpers/video'
48 import { CONFIG } from '../../../initializers/config'
49 import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
50 import { isLocalVideoAccepted } from '../../../lib/moderation'
51 import { Hooks } from '../../../lib/plugins/hooks'
52 import { AccountModel } from '../../../models/account/account'
53 import { VideoModel } from '../../../models/video/video'
54 import { authenticatePromiseIfNeeded } from '../../oauth'
55 import { areValidationErrors } from '../utils'
56 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
58 const videosAddValidator = getCommonVideoEditAttributes().concat([
60 .custom((value, { req }) => isFileFieldValid(req.files, 'videofile'))
61 .withMessage('Should have a file'),
63 .custom(isVideoNameValid)
64 .withMessage('Should have a valid name'),
66 .customSanitizer(toIntOrNull)
67 .custom(isIdValid).withMessage('Should have correct video channel id'),
69 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
70 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
72 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
73 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
75 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
76 const user = res.locals.oauth.token.User
78 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
80 if (!isVideoFileMimeTypeValid(req.files)) {
81 res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415)
83 error: 'This file is not supported. Please, make sure it is of the following type: ' +
84 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
87 return cleanUpReqFiles(req)
90 if (!isVideoFileSizeValid(videoFile.size.toString())) {
91 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
93 error: 'This file is too large.'
96 return cleanUpReqFiles(req)
99 if (await isAbleToUploadVideo(user.id, videoFile.size) === false) {
100 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
101 .json({ error: 'The user video quota is exceeded with this video.' })
103 return cleanUpReqFiles(req)
109 duration = await getDurationFromVideoFile(videoFile.path)
111 logger.error('Invalid input file in videosAddValidator.', { err })
112 res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
113 .json({ error: 'Video file unreadable.' })
115 return cleanUpReqFiles(req)
118 videoFile.duration = duration
120 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
126 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
127 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
130 .custom(isVideoNameValid).withMessage('Should have a valid name'),
133 .customSanitizer(toIntOrNull)
134 .custom(isIdValid).withMessage('Should have correct video channel id'),
136 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
137 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
139 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
140 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
141 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
143 // Check if the user who did the request is able to update the video
144 const user = res.locals.oauth.token.User
145 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
147 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
153 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
154 const video = getVideoWithAttributes(res)
156 // Anybody can watch local videos
157 if (video.isOwned() === true) return next()
160 if (res.locals.oauth) {
161 // Users can search or watch remote videos
162 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
165 // Anybody can search or watch remote videos
166 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
168 // Check our instance follows an actor that shared this video
169 const serverActor = await getServerActor()
170 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
172 return res.status(HttpStatusCode.FORBIDDEN_403)
174 errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
175 error: 'Cannot get this video regarding follow constraints.',
180 const videosCustomGetValidator = (
181 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
182 authenticateInQuery = false
185 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
187 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
188 logger.debug('Checking videosGet parameters', { parameters: req.params })
190 if (areValidationErrors(req, res)) return
191 if (!await doesVideoExist(req.params.id, res, fetchType)) return
193 // Controllers does not need to check video rights
194 if (fetchType === 'only-immutable-attributes') return next()
196 const video = getVideoWithAttributes(res)
197 const videoAll = video as MVideoFullLight
199 // Video private or blacklisted
200 if (videoAll.requiresAuth()) {
201 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
203 const user = res.locals.oauth ? res.locals.oauth.token.User : null
205 // Only the owner or a user that have blacklist rights can see the video
206 if (!user || !user.canGetVideo(videoAll)) {
207 return res.status(HttpStatusCode.FORBIDDEN_403)
208 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
214 // Video is public, anyone can access it
215 if (video.privacy === VideoPrivacy.PUBLIC) return next()
217 // Video is unlisted, check we used the uuid to fetch it
218 if (video.privacy === VideoPrivacy.UNLISTED) {
219 if (isUUIDValid(req.params.id)) return next()
221 // Don't leak this unlisted video
222 return res.status(HttpStatusCode.NOT_FOUND_404).end()
228 const videosGetValidator = videosCustomGetValidator('all')
229 const videosDownloadValidator = videosCustomGetValidator('all', true)
231 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
232 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
233 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
235 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
236 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
238 if (areValidationErrors(req, res)) return
239 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
245 const videosRemoveValidator = [
246 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
248 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
249 logger.debug('Checking videosRemove parameters', { parameters: req.params })
251 if (areValidationErrors(req, res)) return
252 if (!await doesVideoExist(req.params.id, res)) return
254 // Check if the user who did the request is able to delete the video
255 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
261 const videosChangeOwnershipValidator = [
262 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
264 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
265 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
267 if (areValidationErrors(req, res)) return
268 if (!await doesVideoExist(req.params.videoId, res)) return
270 // Check if the user who did the request is able to change the ownership of the video
271 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
273 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
275 res.status(HttpStatusCode.BAD_REQUEST_400)
276 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
280 res.locals.nextOwner = nextOwner
286 const videosTerminateChangeOwnershipValidator = [
287 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
289 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
290 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
292 if (areValidationErrors(req, res)) return
293 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
295 // Check if the user who did the request is able to change the ownership of the video
296 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
298 const videoChangeOwnership = res.locals.videoChangeOwnership
300 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
301 res.status(HttpStatusCode.FORBIDDEN_403)
302 .json({ error: 'Ownership already accepted or refused' })
310 const videosAcceptChangeOwnershipValidator = [
311 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
312 const body = req.body as VideoChangeOwnershipAccept
313 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
315 const user = res.locals.oauth.token.User
316 const videoChangeOwnership = res.locals.videoChangeOwnership
317 const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size)
318 if (isAble === false) {
319 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
320 .json({ error: 'The user video quota is exceeded with this video.' })
329 const videosOverviewValidator = [
332 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
333 .withMessage('Should have a valid pagination'),
335 (req: express.Request, res: express.Response, next: express.NextFunction) => {
336 if (areValidationErrors(req, res)) return
342 function getCommonVideoEditAttributes () {
344 body('thumbnailfile')
345 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
346 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
347 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
350 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
351 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
352 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
357 .customSanitizer(toIntOrNull)
358 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
361 .customSanitizer(toIntOrNull)
362 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
365 .customSanitizer(toValueOrNull)
366 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
369 .customSanitizer(toBooleanOrNull)
370 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
371 body('waitTranscoding')
373 .customSanitizer(toBooleanOrNull)
374 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
377 .customSanitizer(toValueOrNull)
378 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
381 .customSanitizer(toValueOrNull)
382 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
385 .customSanitizer(toValueOrNull)
386 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
389 .customSanitizer(toValueOrNull)
390 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
391 body('commentsEnabled')
393 .customSanitizer(toBooleanOrNull)
394 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
395 body('downloadEnabled')
397 .customSanitizer(toBooleanOrNull)
398 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
399 body('originallyPublishedAt')
401 .customSanitizer(toValueOrNull)
402 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
403 body('scheduleUpdate')
405 .customSanitizer(toValueOrNull),
406 body('scheduleUpdate.updateAt')
408 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
409 body('scheduleUpdate.privacy')
411 .customSanitizer(toIntOrNull)
412 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
413 ] as (ValidationChain | express.Handler)[]
416 const commonVideosFiltersValidator = [
417 query('categoryOneOf')
419 .customSanitizer(toArray)
420 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
421 query('licenceOneOf')
423 .customSanitizer(toArray)
424 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
425 query('languageOneOf')
427 .customSanitizer(toArray)
428 .custom(isStringArray).withMessage('Should have a valid one of language array'),
431 .customSanitizer(toArray)
432 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
435 .customSanitizer(toArray)
436 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
439 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
442 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
445 .customSanitizer(toBooleanOrNull)
446 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
448 (req: express.Request, res: express.Response, next: express.NextFunction) => {
449 logger.debug('Checking commons video filters query', { parameters: req.query })
451 if (areValidationErrors(req, res)) return
453 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
455 (req.query.filter === 'all-local' || req.query.filter === 'all') &&
456 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
458 res.status(HttpStatusCode.UNAUTHORIZED_401)
459 .json({ error: 'You are not allowed to see all local videos.' })
468 // ---------------------------------------------------------------------------
472 videosUpdateValidator,
474 videoFileMetadataGetValidator,
475 videosDownloadValidator,
476 checkVideoFollowConstraints,
477 videosCustomGetValidator,
478 videosRemoveValidator,
480 videosChangeOwnershipValidator,
481 videosTerminateChangeOwnershipValidator,
482 videosAcceptChangeOwnershipValidator,
484 getCommonVideoEditAttributes,
486 commonVideosFiltersValidator,
488 videosOverviewValidator
491 // ---------------------------------------------------------------------------
493 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
494 if (req.body.scheduleUpdate) {
495 if (!req.body.scheduleUpdate.updateAt) {
496 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
498 res.status(HttpStatusCode.BAD_REQUEST_400)
499 .json({ error: 'Schedule update at is mandatory.' })
508 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
509 // Check we accept this video
510 const acceptParameters = {
513 user: res.locals.oauth.token.User
515 const acceptedResult = await Hooks.wrapFun(
516 isLocalVideoAccepted,
518 'filter:api.video.upload.accept.result'
521 if (!acceptedResult || acceptedResult.accepted !== true) {
522 logger.info('Refused local video.', { acceptedResult, acceptParameters })
523 res.status(HttpStatusCode.FORBIDDEN_403)
524 .json({ error: acceptedResult.errorMessage || 'Refused local video' })