videos.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. import * as express from 'express'
  2. import 'express-validator'
  3. import { body, param, query, ValidationChain } from 'express-validator/check'
  4. import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
  5. import {
  6. isBooleanValid,
  7. isDateValid,
  8. isIdOrUUIDValid,
  9. isIdValid,
  10. isUUIDValid,
  11. toArray,
  12. toIntOrNull,
  13. toValueOrNull
  14. } from '../../../helpers/custom-validators/misc'
  15. import {
  16. checkUserCanManageVideo,
  17. doesVideoChannelOfAccountExist,
  18. doesVideoExist,
  19. isScheduleVideoUpdatePrivacyValid,
  20. isVideoCategoryValid,
  21. isVideoDescriptionValid,
  22. isVideoFile,
  23. isVideoFilterValid,
  24. isVideoImage,
  25. isVideoLanguageValid,
  26. isVideoLicenceValid,
  27. isVideoNameValid,
  28. isVideoOriginallyPublishedAtValid,
  29. isVideoPrivacyValid,
  30. isVideoSupportValid,
  31. isVideoTagsValid
  32. } from '../../../helpers/custom-validators/videos'
  33. import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
  34. import { logger } from '../../../helpers/logger'
  35. import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
  36. import { authenticatePromiseIfNeeded } from '../../oauth'
  37. import { areValidationErrors } from '../utils'
  38. import { cleanUpReqFiles } from '../../../helpers/express-utils'
  39. import { VideoModel } from '../../../models/video/video'
  40. import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
  41. import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
  42. import { AccountModel } from '../../../models/account/account'
  43. import { VideoFetchType } from '../../../helpers/video'
  44. import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
  45. import { getServerActor } from '../../../helpers/utils'
  46. import { CONFIG } from '../../../initializers/config'
  47. const videosAddValidator = getCommonVideoEditAttributes().concat([
  48. body('videofile')
  49. .custom((value, { req }) => isVideoFile(req.files)).withMessage(
  50. 'This file is not supported or too large. Please, make sure it is of the following type: '
  51. + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
  52. ),
  53. body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
  54. body('channelId')
  55. .toInt()
  56. .custom(isIdValid).withMessage('Should have correct video channel id'),
  57. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  58. logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
  59. if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
  60. if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
  61. const videoFile: Express.Multer.File = req.files['videofile'][0]
  62. const user = res.locals.oauth.token.User
  63. if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
  64. const isAble = await user.isAbleToUploadVideo(videoFile)
  65. if (isAble === false) {
  66. res.status(403)
  67. .json({ error: 'The user video quota is exceeded with this video.' })
  68. return cleanUpReqFiles(req)
  69. }
  70. let duration: number
  71. try {
  72. duration = await getDurationFromVideoFile(videoFile.path)
  73. } catch (err) {
  74. logger.error('Invalid input file in videosAddValidator.', { err })
  75. res.status(400)
  76. .json({ error: 'Invalid input file.' })
  77. return cleanUpReqFiles(req)
  78. }
  79. videoFile['duration'] = duration
  80. return next()
  81. }
  82. ])
  83. const videosUpdateValidator = getCommonVideoEditAttributes().concat([
  84. param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
  85. body('name')
  86. .optional()
  87. .custom(isVideoNameValid).withMessage('Should have a valid name'),
  88. body('channelId')
  89. .optional()
  90. .toInt()
  91. .custom(isIdValid).withMessage('Should have correct video channel id'),
  92. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  93. logger.debug('Checking videosUpdate parameters', { parameters: req.body })
  94. if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
  95. if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
  96. if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
  97. // Check if the user who did the request is able to update the video
  98. const user = res.locals.oauth.token.User
  99. if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
  100. if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
  101. return next()
  102. }
  103. ])
  104. async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
  105. const video = res.locals.video
  106. // Anybody can watch local videos
  107. if (video.isOwned() === true) return next()
  108. // Logged user
  109. if (res.locals.oauth) {
  110. // Users can search or watch remote videos
  111. if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
  112. }
  113. // Anybody can search or watch remote videos
  114. if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
  115. // Check our instance follows an actor that shared this video
  116. const serverActor = await getServerActor()
  117. if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
  118. return res.status(403)
  119. .json({
  120. error: 'Cannot get this video regarding follow constraints.'
  121. })
  122. }
  123. const videosCustomGetValidator = (fetchType: VideoFetchType) => {
  124. return [
  125. param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
  126. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  127. logger.debug('Checking videosGet parameters', { parameters: req.params })
  128. if (areValidationErrors(req, res)) return
  129. if (!await doesVideoExist(req.params.id, res, fetchType)) return
  130. const video = res.locals.video
  131. // Video private or blacklisted
  132. if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
  133. await authenticatePromiseIfNeeded(req, res)
  134. const user = res.locals.oauth ? res.locals.oauth.token.User : null
  135. // Only the owner or a user that have blacklist rights can see the video
  136. if (
  137. !user ||
  138. (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
  139. ) {
  140. return res.status(403)
  141. .json({ error: 'Cannot get this private or blacklisted video.' })
  142. }
  143. return next()
  144. }
  145. // Video is public, anyone can access it
  146. if (video.privacy === VideoPrivacy.PUBLIC) return next()
  147. // Video is unlisted, check we used the uuid to fetch it
  148. if (video.privacy === VideoPrivacy.UNLISTED) {
  149. if (isUUIDValid(req.params.id)) return next()
  150. // Don't leak this unlisted video
  151. return res.status(404).end()
  152. }
  153. }
  154. ]
  155. }
  156. const videosGetValidator = videosCustomGetValidator('all')
  157. const videosRemoveValidator = [
  158. param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
  159. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  160. logger.debug('Checking videosRemove parameters', { parameters: req.params })
  161. if (areValidationErrors(req, res)) return
  162. if (!await doesVideoExist(req.params.id, res)) return
  163. // Check if the user who did the request is able to delete the video
  164. if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
  165. return next()
  166. }
  167. ]
  168. const videosChangeOwnershipValidator = [
  169. param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
  170. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  171. logger.debug('Checking changeOwnership parameters', { parameters: req.params })
  172. if (areValidationErrors(req, res)) return
  173. if (!await doesVideoExist(req.params.videoId, res)) return
  174. // Check if the user who did the request is able to change the ownership of the video
  175. if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
  176. const nextOwner = await AccountModel.loadLocalByName(req.body.username)
  177. if (!nextOwner) {
  178. res.status(400)
  179. .json({ error: 'Changing video ownership to a remote account is not supported yet' })
  180. return
  181. }
  182. res.locals.nextOwner = nextOwner
  183. return next()
  184. }
  185. ]
  186. const videosTerminateChangeOwnershipValidator = [
  187. 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 changeOwnership parameters', { parameters: req.params })
  190. if (areValidationErrors(req, res)) return
  191. if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
  192. // Check if the user who did the request is able to change the ownership of the video
  193. if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
  194. return next()
  195. },
  196. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  197. const videoChangeOwnership = res.locals.videoChangeOwnership
  198. if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
  199. return next()
  200. } else {
  201. res.status(403)
  202. .json({ error: 'Ownership already accepted or refused' })
  203. return
  204. }
  205. }
  206. ]
  207. const videosAcceptChangeOwnershipValidator = [
  208. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  209. const body = req.body as VideoChangeOwnershipAccept
  210. if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
  211. const user = res.locals.oauth.token.User
  212. const videoChangeOwnership = res.locals.videoChangeOwnership
  213. const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
  214. if (isAble === false) {
  215. res.status(403)
  216. .json({ error: 'The user video quota is exceeded with this video.' })
  217. return
  218. }
  219. return next()
  220. }
  221. ]
  222. function getCommonVideoEditAttributes () {
  223. return [
  224. body('thumbnailfile')
  225. .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
  226. 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
  227. + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
  228. ),
  229. body('previewfile')
  230. .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
  231. 'This preview file is not supported or too large. Please, make sure it is of the following type: '
  232. + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
  233. ),
  234. body('category')
  235. .optional()
  236. .customSanitizer(toIntOrNull)
  237. .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
  238. body('licence')
  239. .optional()
  240. .customSanitizer(toIntOrNull)
  241. .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
  242. body('language')
  243. .optional()
  244. .customSanitizer(toValueOrNull)
  245. .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
  246. body('nsfw')
  247. .optional()
  248. .toBoolean()
  249. .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
  250. body('waitTranscoding')
  251. .optional()
  252. .toBoolean()
  253. .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
  254. body('privacy')
  255. .optional()
  256. .toInt()
  257. .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
  258. body('description')
  259. .optional()
  260. .customSanitizer(toValueOrNull)
  261. .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
  262. body('support')
  263. .optional()
  264. .customSanitizer(toValueOrNull)
  265. .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
  266. body('tags')
  267. .optional()
  268. .customSanitizer(toValueOrNull)
  269. .custom(isVideoTagsValid).withMessage('Should have correct tags'),
  270. body('commentsEnabled')
  271. .optional()
  272. .toBoolean()
  273. .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
  274. body('downloadEnabled')
  275. .optional()
  276. .toBoolean()
  277. .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
  278. body('originallyPublishedAt')
  279. .optional()
  280. .customSanitizer(toValueOrNull)
  281. .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
  282. body('scheduleUpdate')
  283. .optional()
  284. .customSanitizer(toValueOrNull),
  285. body('scheduleUpdate.updateAt')
  286. .optional()
  287. .custom(isDateValid).withMessage('Should have a valid schedule update date'),
  288. body('scheduleUpdate.privacy')
  289. .optional()
  290. .toInt()
  291. .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
  292. ] as (ValidationChain | express.Handler)[]
  293. }
  294. const commonVideosFiltersValidator = [
  295. query('categoryOneOf')
  296. .optional()
  297. .customSanitizer(toArray)
  298. .custom(isNumberArray).withMessage('Should have a valid one of category array'),
  299. query('licenceOneOf')
  300. .optional()
  301. .customSanitizer(toArray)
  302. .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
  303. query('languageOneOf')
  304. .optional()
  305. .customSanitizer(toArray)
  306. .custom(isStringArray).withMessage('Should have a valid one of language array'),
  307. query('tagsOneOf')
  308. .optional()
  309. .customSanitizer(toArray)
  310. .custom(isStringArray).withMessage('Should have a valid one of tags array'),
  311. query('tagsAllOf')
  312. .optional()
  313. .customSanitizer(toArray)
  314. .custom(isStringArray).withMessage('Should have a valid all of tags array'),
  315. query('nsfw')
  316. .optional()
  317. .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
  318. query('filter')
  319. .optional()
  320. .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
  321. (req: express.Request, res: express.Response, next: express.NextFunction) => {
  322. logger.debug('Checking commons video filters query', { parameters: req.query })
  323. if (areValidationErrors(req, res)) return
  324. const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
  325. if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
  326. res.status(401)
  327. .json({ error: 'You are not allowed to see all local videos.' })
  328. return
  329. }
  330. return next()
  331. }
  332. ]
  333. // ---------------------------------------------------------------------------
  334. export {
  335. videosAddValidator,
  336. videosUpdateValidator,
  337. videosGetValidator,
  338. checkVideoFollowConstraints,
  339. videosCustomGetValidator,
  340. videosRemoveValidator,
  341. videosChangeOwnershipValidator,
  342. videosTerminateChangeOwnershipValidator,
  343. videosAcceptChangeOwnershipValidator,
  344. getCommonVideoEditAttributes,
  345. commonVideosFiltersValidator
  346. }
  347. // ---------------------------------------------------------------------------
  348. function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
  349. if (req.body.scheduleUpdate) {
  350. if (!req.body.scheduleUpdate.updateAt) {
  351. logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
  352. res.status(400)
  353. .json({ error: 'Schedule update at is mandatory.' })
  354. return true
  355. }
  356. }
  357. return false
  358. }