From 6e46de095d7169355dd83030f6ce4a582304153a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 5 Oct 2018 11:15:06 +0200 Subject: Add user history and resume videos --- server/middlewares/validators/videos/index.ts | 8 + .../middlewares/validators/videos/video-abuses.ts | 71 ++++ .../validators/videos/video-blacklist.ts | 62 ++++ .../validators/videos/video-captions.ts | 71 ++++ .../validators/videos/video-channels.ts | 175 +++++++++ .../validators/videos/video-comments.ts | 195 ++++++++++ .../middlewares/validators/videos/video-imports.ts | 75 ++++ .../middlewares/validators/videos/video-watch.ts | 28 ++ server/middlewares/validators/videos/videos.ts | 399 +++++++++++++++++++++ 9 files changed, 1084 insertions(+) create mode 100644 server/middlewares/validators/videos/index.ts create mode 100644 server/middlewares/validators/videos/video-abuses.ts create mode 100644 server/middlewares/validators/videos/video-blacklist.ts create mode 100644 server/middlewares/validators/videos/video-captions.ts create mode 100644 server/middlewares/validators/videos/video-channels.ts create mode 100644 server/middlewares/validators/videos/video-comments.ts create mode 100644 server/middlewares/validators/videos/video-imports.ts create mode 100644 server/middlewares/validators/videos/video-watch.ts create mode 100644 server/middlewares/validators/videos/videos.ts (limited to 'server/middlewares/validators/videos') diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts new file mode 100644 index 000000000..294783d85 --- /dev/null +++ b/server/middlewares/validators/videos/index.ts @@ -0,0 +1,8 @@ +export * from './video-abuses' +export * from './video-blacklist' +export * from './video-captions' +export * from './video-channels' +export * from './video-comments' +export * from './video-imports' +export * from './video-watch' +export * from './videos' diff --git a/server/middlewares/validators/videos/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts new file mode 100644 index 000000000..be26ca16a --- /dev/null +++ b/server/middlewares/validators/videos/video-abuses.ts @@ -0,0 +1,71 @@ +import * as express from 'express' +import 'express-validator' +import { body, param } from 'express-validator/check' +import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' +import { isVideoExist } from '../../../helpers/custom-validators/videos' +import { logger } from '../../../helpers/logger' +import { areValidationErrors } from '../utils' +import { + isVideoAbuseExist, + isVideoAbuseModerationCommentValid, + isVideoAbuseReasonValid, + isVideoAbuseStateValid +} from '../../../helpers/custom-validators/video-abuses' + +const videoAbuseReportValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + + return next() + } +] + +const videoAbuseGetValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + if (!await isVideoAbuseExist(req.params.id, res.locals.video.id, res)) return + + return next() + } +] + +const videoAbuseUpdateValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), + body('state') + .optional() + .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'), + body('moderationComment') + .optional() + .custom(isVideoAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + if (!await isVideoAbuseExist(req.params.id, res.locals.video.id, res)) return + + return next() + } +] + +// --------------------------------------------------------------------------- + +export { + videoAbuseReportValidator, + videoAbuseGetValidator, + videoAbuseUpdateValidator +} diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts new file mode 100644 index 000000000..13da7acff --- /dev/null +++ b/server/middlewares/validators/videos/video-blacklist.ts @@ -0,0 +1,62 @@ +import * as express from 'express' +import { body, param } from 'express-validator/check' +import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' +import { isVideoExist } from '../../../helpers/custom-validators/videos' +import { logger } from '../../../helpers/logger' +import { areValidationErrors } from '../utils' +import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' + +const videosBlacklistRemoveValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking blacklistRemove parameters.', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + if (!await isVideoBlacklistExist(res.locals.video.id, res)) return + + return next() + } +] + +const videosBlacklistAddValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + body('reason') + .optional() + .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videosBlacklistAdd parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + + return next() + } +] + +const videosBlacklistUpdateValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + body('reason') + .optional() + .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videosBlacklistUpdate parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + if (!await isVideoBlacklistExist(res.locals.video.id, res)) return + + return next() + } +] + +// --------------------------------------------------------------------------- + +export { + videosBlacklistAddValidator, + videosBlacklistRemoveValidator, + videosBlacklistUpdateValidator +} diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts new file mode 100644 index 000000000..63d84fbec --- /dev/null +++ b/server/middlewares/validators/videos/video-captions.ts @@ -0,0 +1,71 @@ +import * as express from 'express' +import { areValidationErrors } from '../utils' +import { checkUserCanManageVideo, isVideoExist } from '../../../helpers/custom-validators/videos' +import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' +import { body, param } from 'express-validator/check' +import { CONSTRAINTS_FIELDS } from '../../../initializers' +import { UserRight } from '../../../../shared' +import { logger } from '../../../helpers/logger' +import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions' +import { cleanUpReqFiles } from '../../../helpers/express-utils' + +const addVideoCaptionValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), + param('captionLanguage').custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), + body('captionfile') + .custom((value, { req }) => isVideoCaptionFile(req.files, 'captionfile')).withMessage( + 'This caption file is not supported or too large. Please, make sure it is of the following type : ' + + CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME.join(', ') + ), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking addVideoCaption parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return cleanUpReqFiles(req) + if (!await isVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req) + + // Check if the user who did the request is able to update the video + const user = res.locals.oauth.token.User + if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) + + return next() + } +] + +const deleteVideoCaptionValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), + param('captionLanguage').custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking deleteVideoCaption parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + if (!await isVideoCaptionExist(res.locals.video, req.params.captionLanguage, res)) return + + // Check if the user who did the request is able to update the video + const user = res.locals.oauth.token.User + if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return + + return next() + } +] + +const listVideoCaptionsValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking listVideoCaptions parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res, 'id')) return + + return next() + } +] + +export { + addVideoCaptionValidator, + listVideoCaptionsValidator, + deleteVideoCaptionValidator +} diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts new file mode 100644 index 000000000..f039794e0 --- /dev/null +++ b/server/middlewares/validators/videos/video-channels.ts @@ -0,0 +1,175 @@ +import * as express from 'express' +import { body, param } from 'express-validator/check' +import { UserRight } from '../../../../shared' +import { isAccountNameWithHostExist } from '../../../helpers/custom-validators/accounts' +import { + isLocalVideoChannelNameExist, + isVideoChannelDescriptionValid, + isVideoChannelNameValid, + isVideoChannelNameWithHostExist, + isVideoChannelSupportValid +} from '../../../helpers/custom-validators/video-channels' +import { logger } from '../../../helpers/logger' +import { UserModel } from '../../../models/account/user' +import { VideoChannelModel } from '../../../models/video/video-channel' +import { areValidationErrors } from '../utils' +import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor' +import { ActorModel } from '../../../models/activitypub/actor' + +const listVideoAccountChannelsValidator = [ + param('accountName').exists().withMessage('Should have a valid account name'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking listVideoAccountChannelsValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isAccountNameWithHostExist(req.params.accountName, res)) return + + return next() + } +] + +const videoChannelsAddValidator = [ + body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'), + body('displayName').custom(isVideoChannelNameValid).withMessage('Should have a valid display name'), + body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'), + body('support').optional().custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoChannelsAdd parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + + const actor = await ActorModel.loadLocalByName(req.body.name) + if (actor) { + res.status(409) + .send({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) + .end() + return false + } + + return next() + } +] + +const videoChannelsUpdateValidator = [ + param('nameWithHost').exists().withMessage('Should have an video channel name with host'), + body('displayName').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid display name'), + body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'), + body('support').optional().custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return + + // We need to make additional checks + if (res.locals.videoChannel.Actor.isOwned() === false) { + return res.status(403) + .json({ error: 'Cannot update video channel of another server' }) + .end() + } + + if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { + return res.status(403) + .json({ error: 'Cannot update video channel of another user' }) + .end() + } + + return next() + } +] + +const videoChannelsRemoveValidator = [ + param('nameWithHost').exists().withMessage('Should have an video channel name with host'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return + + if (!checkUserCanDeleteVideoChannel(res.locals.oauth.token.User, res.locals.videoChannel, res)) return + if (!await checkVideoChannelIsNotTheLastOne(res)) return + + return next() + } +] + +const videoChannelsNameWithHostValidator = [ + param('nameWithHost').exists().withMessage('Should have an video channel name with host'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoChannelsNameWithHostValidator parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + + if (!await isVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return + + return next() + } +] + +const localVideoChannelValidator = [ + param('name').custom(isVideoChannelNameValid).withMessage('Should have a valid video channel name'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking localVideoChannelValidator parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isLocalVideoChannelNameExist(req.params.name, res)) return + + return next() + } +] + +// --------------------------------------------------------------------------- + +export { + listVideoAccountChannelsValidator, + videoChannelsAddValidator, + videoChannelsUpdateValidator, + videoChannelsRemoveValidator, + videoChannelsNameWithHostValidator, + localVideoChannelValidator +} + +// --------------------------------------------------------------------------- + +function checkUserCanDeleteVideoChannel (user: UserModel, videoChannel: VideoChannelModel, res: express.Response) { + if (videoChannel.Actor.isOwned() === false) { + res.status(403) + .json({ error: 'Cannot remove video channel of another server.' }) + .end() + + return false + } + + // Check if the user can delete the video channel + // The user can delete it if s/he is an admin + // Or if s/he is the video channel's account + if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) { + res.status(403) + .json({ error: 'Cannot remove video channel of another user' }) + .end() + + return false + } + + return true +} + +async function checkVideoChannelIsNotTheLastOne (res: express.Response) { + const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) + + if (count <= 1) { + res.status(409) + .json({ error: 'Cannot remove the last channel of this user' }) + .end() + + return false + } + + return true +} diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts new file mode 100644 index 000000000..348d33082 --- /dev/null +++ b/server/middlewares/validators/videos/video-comments.ts @@ -0,0 +1,195 @@ +import * as express from 'express' +import { body, param } from 'express-validator/check' +import { UserRight } from '../../../../shared' +import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' +import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' +import { isVideoExist } from '../../../helpers/custom-validators/videos' +import { logger } from '../../../helpers/logger' +import { UserModel } from '../../../models/account/user' +import { VideoModel } from '../../../models/video/video' +import { VideoCommentModel } from '../../../models/video/video-comment' +import { areValidationErrors } from '../utils' + +const listVideoCommentThreadsValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res, 'only-video')) return + + return next() + } +] + +const listVideoThreadCommentsValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('threadId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking listVideoThreadComments parameters.', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res, 'only-video')) return + if (!await isVideoCommentThreadExist(req.params.threadId, res.locals.video, res)) return + + return next() + } +] + +const addVideoCommentThreadValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params, body: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + if (!isVideoCommentsEnabled(res.locals.video, res)) return + + return next() + } +] + +const addVideoCommentReplyValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), + body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking addVideoCommentReply parameters.', { parameters: req.params, body: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + if (!isVideoCommentsEnabled(res.locals.video, res)) return + if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return + + return next() + } +] + +const videoCommentGetValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res, 'id')) return + if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return + + return next() + } +] + +const removeVideoCommentValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking removeVideoCommentValidator parameters.', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return + + // Check if the user who did the request is able to delete the video + if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoComment, res)) return + + return next() + } +] + +// --------------------------------------------------------------------------- + +export { + listVideoCommentThreadsValidator, + listVideoThreadCommentsValidator, + addVideoCommentThreadValidator, + addVideoCommentReplyValidator, + videoCommentGetValidator, + removeVideoCommentValidator +} + +// --------------------------------------------------------------------------- + +async function isVideoCommentThreadExist (id: number, video: VideoModel, res: express.Response) { + const videoComment = await VideoCommentModel.loadById(id) + + if (!videoComment) { + res.status(404) + .json({ error: 'Video comment thread not found' }) + .end() + + return false + } + + if (videoComment.videoId !== video.id) { + res.status(400) + .json({ error: 'Video comment is associated to this video.' }) + .end() + + return false + } + + if (videoComment.inReplyToCommentId !== null) { + res.status(400) + .json({ error: 'Video comment is not a thread.' }) + .end() + + return false + } + + res.locals.videoCommentThread = videoComment + return true +} + +async function isVideoCommentExist (id: number, video: VideoModel, res: express.Response) { + const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) + + if (!videoComment) { + res.status(404) + .json({ error: 'Video comment thread not found' }) + .end() + + return false + } + + if (videoComment.videoId !== video.id) { + res.status(400) + .json({ error: 'Video comment is associated to this video.' }) + .end() + + return false + } + + res.locals.videoComment = videoComment + return true +} + +function isVideoCommentsEnabled (video: VideoModel, res: express.Response) { + if (video.commentsEnabled !== true) { + res.status(409) + .json({ error: 'Video comments are disabled for this video.' }) + .end() + + return false + } + + return true +} + +function checkUserCanDeleteVideoComment (user: UserModel, videoComment: VideoCommentModel, res: express.Response) { + const account = videoComment.Account + if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && account.userId !== user.id) { + res.status(403) + .json({ error: 'Cannot remove video comment of another user' }) + .end() + return false + } + + return true +} diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts new file mode 100644 index 000000000..48d20f904 --- /dev/null +++ b/server/middlewares/validators/videos/video-imports.ts @@ -0,0 +1,75 @@ +import * as express from 'express' +import { body } from 'express-validator/check' +import { isIdValid } from '../../../helpers/custom-validators/misc' +import { logger } from '../../../helpers/logger' +import { areValidationErrors } from '../utils' +import { getCommonVideoAttributes } from './videos' +import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' +import { cleanUpReqFiles } from '../../../helpers/express-utils' +import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos' +import { CONFIG } from '../../../initializers/constants' +import { CONSTRAINTS_FIELDS } from '../../../initializers' + +const videoImportAddValidator = getCommonVideoAttributes().concat([ + body('channelId') + .toInt() + .custom(isIdValid).withMessage('Should have correct video channel id'), + body('targetUrl') + .optional() + .custom(isVideoImportTargetUrlValid).withMessage('Should have a valid video import target URL'), + body('magnetUri') + .optional() + .custom(isVideoMagnetUriValid).withMessage('Should have a valid video magnet URI'), + body('torrentfile') + .custom((value, { req }) => isVideoImportTorrentFile(req.files)).withMessage( + 'This torrent file is not supported or too large. Please, make sure it is of the following type: ' + + CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.EXTNAME.join(', ') + ), + body('name') + .optional() + .custom(isVideoNameValid).withMessage('Should have a valid name'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoImportAddValidator parameters', { parameters: req.body }) + + const user = res.locals.oauth.token.User + const torrentFile = req.files && req.files['torrentfile'] ? req.files['torrentfile'][0] : undefined + + if (areValidationErrors(req, res)) return cleanUpReqFiles(req) + + if (req.body.targetUrl && CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true) { + cleanUpReqFiles(req) + return res.status(409) + .json({ error: 'HTTP import is not enabled on this instance.' }) + .end() + } + + if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) { + cleanUpReqFiles(req) + return res.status(409) + .json({ error: 'Torrent/magnet URI import is not enabled on this instance.' }) + .end() + } + + if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) + + // Check we have at least 1 required param + if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) { + cleanUpReqFiles(req) + + return res.status(400) + .json({ error: 'Should have a magnetUri or a targetUrl or a torrent file.' }) + .end() + } + + return next() + } +]) + +// --------------------------------------------------------------------------- + +export { + videoImportAddValidator +} + +// --------------------------------------------------------------------------- diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts new file mode 100644 index 000000000..bca64662f --- /dev/null +++ b/server/middlewares/validators/videos/video-watch.ts @@ -0,0 +1,28 @@ +import { body, param } from 'express-validator/check' +import * as express from 'express' +import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' +import { isVideoExist } from '../../../helpers/custom-validators/videos' +import { areValidationErrors } from '../utils' +import { logger } from '../../../helpers/logger' + +const videoWatchingValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + body('currentTime') + .toInt() + .isInt().withMessage('Should have correct current time'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoWatching parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res, 'id')) return + + return next() + } +] + +// --------------------------------------------------------------------------- + +export { + videoWatchingValidator +} diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts new file mode 100644 index 000000000..d6b8aa725 --- /dev/null +++ b/server/middlewares/validators/videos/videos.ts @@ -0,0 +1,399 @@ +import * as express from 'express' +import 'express-validator' +import { body, param, ValidationChain } from 'express-validator/check' +import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' +import { + isBooleanValid, + isDateValid, + isIdOrUUIDValid, + isIdValid, + isUUIDValid, + toIntOrNull, + toValueOrNull +} from '../../../helpers/custom-validators/misc' +import { + checkUserCanManageVideo, + isScheduleVideoUpdatePrivacyValid, + isVideoCategoryValid, + isVideoChannelOfAccountExist, + isVideoDescriptionValid, + isVideoExist, + isVideoFile, + isVideoImage, + isVideoLanguageValid, + isVideoLicenceValid, + isVideoNameValid, + isVideoPrivacyValid, + isVideoRatingTypeValid, + isVideoSupportValid, + isVideoTagsValid +} from '../../../helpers/custom-validators/videos' +import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' +import { logger } from '../../../helpers/logger' +import { CONSTRAINTS_FIELDS } from '../../../initializers' +import { VideoShareModel } from '../../../models/video/video-share' +import { authenticate } from '../../oauth' +import { areValidationErrors } from '../utils' +import { cleanUpReqFiles } from '../../../helpers/express-utils' +import { VideoModel } from '../../../models/video/video' +import { UserModel } from '../../../models/account/user' +import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' +import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' +import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' +import { AccountModel } from '../../../models/account/account' +import { VideoFetchType } from '../../../helpers/video' + +const videosAddValidator = getCommonVideoAttributes().concat([ + body('videofile') + .custom((value, { req }) => isVideoFile(req.files)).withMessage( + 'This file is not supported or too large. Please, make sure it is of the following type: ' + + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') + ), + body('name').custom(isVideoNameValid).withMessage('Should have a valid name'), + body('channelId') + .toInt() + .custom(isIdValid).withMessage('Should have correct video channel id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) + + if (areValidationErrors(req, res)) return cleanUpReqFiles(req) + if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) + + const videoFile: Express.Multer.File = req.files['videofile'][0] + const user = res.locals.oauth.token.User + + if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) + + const isAble = await user.isAbleToUploadVideo(videoFile) + if (isAble === false) { + res.status(403) + .json({ error: 'The user video quota is exceeded with this video.' }) + .end() + + return cleanUpReqFiles(req) + } + + let duration: number + + try { + duration = await getDurationFromVideoFile(videoFile.path) + } catch (err) { + logger.error('Invalid input file in videosAddValidator.', { err }) + res.status(400) + .json({ error: 'Invalid input file.' }) + .end() + + return cleanUpReqFiles(req) + } + + videoFile['duration'] = duration + + return next() + } +]) + +const videosUpdateValidator = getCommonVideoAttributes().concat([ + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + body('name') + .optional() + .custom(isVideoNameValid).withMessage('Should have a valid name'), + body('channelId') + .optional() + .toInt() + .custom(isIdValid).withMessage('Should have correct video channel id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videosUpdate parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return cleanUpReqFiles(req) + if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) + if (!await isVideoExist(req.params.id, res)) return cleanUpReqFiles(req) + + const video = res.locals.video + + // Check if the user who did the request is able to update the video + const user = res.locals.oauth.token.User + if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) + + if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) { + cleanUpReqFiles(req) + return res.status(409) + .json({ error: 'Cannot set "private" a video that was not private.' }) + .end() + } + + if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) + + return next() + } +]) + +const videosCustomGetValidator = (fetchType: VideoFetchType) => { + return [ + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videosGet parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.id, res, fetchType)) return + + const video: VideoModel = res.locals.video + + // Video private or blacklisted + if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { + return authenticate(req, res, () => { + const user: UserModel = res.locals.oauth.token.User + + // Only the owner or a user that have blacklist rights can see the video + if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) { + return res.status(403) + .json({ error: 'Cannot get this private or blacklisted video.' }) + .end() + } + + return next() + }) + } + + // Video is public, anyone can access it + if (video.privacy === VideoPrivacy.PUBLIC) return next() + + // Video is unlisted, check we used the uuid to fetch it + if (video.privacy === VideoPrivacy.UNLISTED) { + if (isUUIDValid(req.params.id)) return next() + + // Don't leak this unlisted video + return res.status(404).end() + } + } + ] +} + +const videosGetValidator = videosCustomGetValidator('all') + +const videosRemoveValidator = [ + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videosRemove parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.id, res)) return + + // Check if the user who did the request is able to delete the video + if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return + + return next() + } +] + +const videoRateValidator = [ + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoRate parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.id, res)) return + + return next() + } +] + +const videosShareValidator = [ + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoShare parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.id, res)) return + + const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined) + if (!share) { + return res.status(404) + .end() + } + + res.locals.videoShare = share + return next() + } +] + +const videosChangeOwnershipValidator = [ + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking changeOwnership parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExist(req.params.videoId, res)) return + + // Check if the user who did the request is able to change the ownership of the video + if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return + + const nextOwner = await AccountModel.loadLocalByName(req.body.username) + if (!nextOwner) { + res.status(400) + .type('json') + .end() + return + } + res.locals.nextOwner = nextOwner + + return next() + } +] + +const videosTerminateChangeOwnershipValidator = [ + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking changeOwnership parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return + + // Check if the user who did the request is able to change the ownership of the video + if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return + + return next() + }, + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel + + if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) { + return next() + } else { + res.status(403) + .json({ error: 'Ownership already accepted or refused' }) + .end() + return + } + } +] + +const videosAcceptChangeOwnershipValidator = [ + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + const body = req.body as VideoChangeOwnershipAccept + if (!await isVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return + + const user = res.locals.oauth.token.User + const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel + const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile()) + if (isAble === false) { + res.status(403) + .json({ error: 'The user video quota is exceeded with this video.' }) + .end() + return + } + + return next() + } +] + +function getCommonVideoAttributes () { + return [ + body('thumbnailfile') + .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage( + 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' + + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') + ), + body('previewfile') + .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage( + 'This preview file is not supported or too large. Please, make sure it is of the following type: ' + + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') + ), + + body('category') + .optional() + .customSanitizer(toIntOrNull) + .custom(isVideoCategoryValid).withMessage('Should have a valid category'), + body('licence') + .optional() + .customSanitizer(toIntOrNull) + .custom(isVideoLicenceValid).withMessage('Should have a valid licence'), + body('language') + .optional() + .customSanitizer(toValueOrNull) + .custom(isVideoLanguageValid).withMessage('Should have a valid language'), + body('nsfw') + .optional() + .toBoolean() + .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'), + body('waitTranscoding') + .optional() + .toBoolean() + .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'), + body('privacy') + .optional() + .toInt() + .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'), + body('description') + .optional() + .customSanitizer(toValueOrNull) + .custom(isVideoDescriptionValid).withMessage('Should have a valid description'), + body('support') + .optional() + .customSanitizer(toValueOrNull) + .custom(isVideoSupportValid).withMessage('Should have a valid support text'), + body('tags') + .optional() + .customSanitizer(toValueOrNull) + .custom(isVideoTagsValid).withMessage('Should have correct tags'), + body('commentsEnabled') + .optional() + .toBoolean() + .custom(isBooleanValid).withMessage('Should have comments enabled boolean'), + + body('scheduleUpdate') + .optional() + .customSanitizer(toValueOrNull), + body('scheduleUpdate.updateAt') + .optional() + .custom(isDateValid).withMessage('Should have a valid schedule update date'), + body('scheduleUpdate.privacy') + .optional() + .toInt() + .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy') + ] as (ValidationChain | express.Handler)[] +} + +// --------------------------------------------------------------------------- + +export { + videosAddValidator, + videosUpdateValidator, + videosGetValidator, + videosCustomGetValidator, + videosRemoveValidator, + videosShareValidator, + + videoRateValidator, + + videosChangeOwnershipValidator, + videosTerminateChangeOwnershipValidator, + videosAcceptChangeOwnershipValidator, + + getCommonVideoAttributes +} + +// --------------------------------------------------------------------------- + +function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { + if (req.body.scheduleUpdate) { + if (!req.body.scheduleUpdate.updateAt) { + res.status(400) + .json({ error: 'Schedule update at is mandatory.' }) + .end() + + return true + } + } + + return false +} -- cgit v1.2.3