Add shares forward and collection on videos/video channels
[oweals/peertube.git] / server / middlewares / validators / videos.ts
1 import * as express from 'express'
2 import { body, param, query } from 'express-validator/check'
3 import { UserRight, VideoPrivacy } from '../../../shared'
4 import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
5 import {
6   checkVideoExists,
7   isVideoAbuseReasonValid,
8   isVideoCategoryValid,
9   isVideoDescriptionValid,
10   isVideoDurationValid,
11   isVideoFile,
12   isVideoLanguageValid,
13   isVideoLicenceValid,
14   isVideoNameValid,
15   isVideoNSFWValid,
16   isVideoPrivacyValid,
17   isVideoRatingTypeValid,
18   isVideoTagsValid
19 } from '../../helpers/custom-validators/videos'
20 import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
21 import { logger } from '../../helpers/logger'
22 import { CONSTRAINTS_FIELDS, SEARCHABLE_COLUMNS } from '../../initializers'
23
24 import { database as db } from '../../initializers/database'
25 import { UserInstance } from '../../models/account/user-interface'
26 import { authenticate } from '../oauth'
27 import { areValidationErrors, checkErrors } from './utils'
28 import { isVideoExistsPromise } from '../../helpers/index'
29
30 const videosAddValidator = [
31   body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage(
32     'This file is not supported. Please, make sure it is of the following type : '
33     + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
34   ),
35   body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
36   body('category').custom(isVideoCategoryValid).withMessage('Should have a valid category'),
37   body('licence').custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
38   body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
39   body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
40   body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
41   body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'),
42   body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
43   body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
44
45   (req: express.Request, res: express.Response, next: express.NextFunction) => {
46     logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
47
48     checkErrors(req, res, () => {
49       const videoFile: Express.Multer.File = req.files['videofile'][0]
50       const user = res.locals.oauth.token.User
51
52       return db.VideoChannel.loadByIdAndAccount(req.body.channelId, user.Account.id)
53         .then(videoChannel => {
54           if (!videoChannel) {
55             res.status(400)
56               .json({ error: 'Unknown video video channel for this account.' })
57               .end()
58
59             return undefined
60           }
61
62           res.locals.videoChannel = videoChannel
63
64           return user.isAbleToUploadVideo(videoFile)
65         })
66         .then(isAble => {
67           if (isAble === false) {
68             res.status(403)
69                .json({ error: 'The user video quota is exceeded with this video.' })
70                .end()
71
72             return undefined
73           }
74
75           return getDurationFromVideoFile(videoFile.path)
76             .catch(err => {
77               logger.error('Invalid input file in videosAddValidator.', err)
78               res.status(400)
79                  .json({ error: 'Invalid input file.' })
80                  .end()
81
82               return undefined
83             })
84         })
85         .then(duration => {
86           // Previous test failed, abort
87           if (duration === undefined) return undefined
88
89           if (!isVideoDurationValid('' + duration)) {
90             return res.status(400)
91                       .json({
92                         error: 'Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).'
93                       })
94                       .end()
95           }
96
97           videoFile['duration'] = duration
98           next()
99         })
100         .catch(err => {
101           logger.error('Error in video add validator', err)
102           res.sendStatus(500)
103
104           return undefined
105         })
106     })
107   }
108 ]
109
110 const videosUpdateValidator = [
111   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
112   body('name').optional().custom(isVideoNameValid).withMessage('Should have a valid name'),
113   body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'),
114   body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
115   body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
116   body('nsfw').optional().custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
117   body('privacy').optional().custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
118   body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
119   body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
120
121   (req: express.Request, res: express.Response, next: express.NextFunction) => {
122     logger.debug('Checking videosUpdate parameters', { parameters: req.body })
123
124     checkErrors(req, res, () => {
125       checkVideoExists(req.params.id, res, () => {
126         const video = res.locals.video
127
128         // We need to make additional checks
129         if (video.isOwned() === false) {
130           return res.status(403)
131                     .json({ error: 'Cannot update video of another server' })
132                     .end()
133         }
134
135         if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
136           return res.status(403)
137                     .json({ error: 'Cannot update video of another user' })
138                     .end()
139         }
140
141         if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
142           return res.status(409)
143             .json({ error: 'Cannot set "private" a video that was not private anymore.' })
144             .end()
145         }
146
147         next()
148       })
149     })
150   }
151 ]
152
153 const videosGetValidator = [
154   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
155
156   (req: express.Request, res: express.Response, next: express.NextFunction) => {
157     logger.debug('Checking videosGet parameters', { parameters: req.params })
158
159     checkErrors(req, res, () => {
160       checkVideoExists(req.params.id, res, () => {
161         const video = res.locals.video
162
163         // Video is not private, anyone can access it
164         if (video.privacy !== VideoPrivacy.PRIVATE) return next()
165
166         authenticate(req, res, () => {
167           if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
168             return res.status(403)
169               .json({ error: 'Cannot get this private video of another user' })
170               .end()
171           }
172
173           next()
174         })
175       })
176     })
177   }
178 ]
179
180 const videosRemoveValidator = [
181   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
182
183   (req: express.Request, res: express.Response, next: express.NextFunction) => {
184     logger.debug('Checking videosRemove parameters', { parameters: req.params })
185
186     checkErrors(req, res, () => {
187       checkVideoExists(req.params.id, res, () => {
188         // Check if the user who did the request is able to delete the video
189         checkUserCanDeleteVideo(res.locals.oauth.token.User, res, () => {
190           next()
191         })
192       })
193     })
194   }
195 ]
196
197 const videosSearchValidator = [
198   param('value').not().isEmpty().withMessage('Should have a valid search'),
199   query('field').optional().isIn(SEARCHABLE_COLUMNS.VIDEOS).withMessage('Should have correct searchable column'),
200
201   (req: express.Request, res: express.Response, next: express.NextFunction) => {
202     logger.debug('Checking videosSearch parameters', { parameters: req.params })
203
204     checkErrors(req, res, next)
205   }
206 ]
207
208 const videoAbuseReportValidator = [
209   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
210   body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
211
212   (req: express.Request, res: express.Response, next: express.NextFunction) => {
213     logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
214
215     checkErrors(req, res, () => {
216       checkVideoExists(req.params.id, res, next)
217     })
218   }
219 ]
220
221 const videoRateValidator = [
222   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
223   body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
224
225   (req: express.Request, res: express.Response, next: express.NextFunction) => {
226     logger.debug('Checking videoRate parameters', { parameters: req.body })
227
228     checkErrors(req, res, () => {
229       checkVideoExists(req.params.id, res, next)
230     })
231   }
232 ]
233
234 const videosShareValidator = [
235   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
236   param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
237
238   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
239     logger.debug('Checking videoShare parameters', { parameters: req.params })
240
241     if (areValidationErrors(req, res)) return
242     if (!await isVideoExistsPromise(req.params.id, res)) return
243
244     const share = await db.VideoShare.load(req.params.accountId, res.locals.video.id)
245     if (!share) {
246       return res.status(404)
247         .end()
248     }
249
250     res.locals.videoShare = share
251
252     return next()
253   }
254 ]
255
256 // ---------------------------------------------------------------------------
257
258 export {
259   videosAddValidator,
260   videosUpdateValidator,
261   videosGetValidator,
262   videosRemoveValidator,
263   videosSearchValidator,
264   videosShareValidator,
265
266   videoAbuseReportValidator,
267
268   videoRateValidator
269 }
270
271 // ---------------------------------------------------------------------------
272
273 function checkUserCanDeleteVideo (user: UserInstance, res: express.Response, callback: () => void) {
274   // Retrieve the user who did the request
275   if (res.locals.video.isOwned() === false) {
276     return res.status(403)
277               .json({ error: 'Cannot remove video of another server, blacklist it' })
278               .end()
279   }
280
281   // Check if the user can delete the video
282   // The user can delete it if s/he is an admin
283   // Or if s/he is the video's account
284   const account = res.locals.video.VideoChannel.Account
285   if (user.hasRight(UserRight.REMOVE_ANY_VIDEO) === false && account.userId !== user.id) {
286     return res.status(403)
287               .json({ error: 'Cannot remove video of another user' })
288               .end()
289   }
290
291   // If we reach this comment, we can delete the video
292   callback()
293 }