Optimize SQL requests of watch page API endpoints
[oweals/peertube.git] / server / helpers / custom-validators / videos.ts
1 import { Response } from 'express'
2 import 'express-validator'
3 import { values } from 'lodash'
4 import 'multer'
5 import * as validator from 'validator'
6 import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared'
7 import {
8   CONSTRAINTS_FIELDS,
9   VIDEO_CATEGORIES,
10   VIDEO_LICENCES,
11   VIDEO_MIMETYPE_EXT,
12   VIDEO_PRIVACIES,
13   VIDEO_RATE_TYPES,
14   VIDEO_STATES
15 } from '../../initializers'
16 import { VideoModel } from '../../models/video/video'
17 import { exists, isArray, isFileValid } from './misc'
18 import { VideoChannelModel } from '../../models/video/video-channel'
19 import { UserModel } from '../../models/account/user'
20 import * as magnetUtil from 'magnet-uri'
21
22 const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
23
24 function isVideoCategoryValid (value: any) {
25   return value === null || VIDEO_CATEGORIES[ value ] !== undefined
26 }
27
28 function isVideoStateValid (value: any) {
29   return exists(value) && VIDEO_STATES[ value ] !== undefined
30 }
31
32 function isVideoLicenceValid (value: any) {
33   return value === null || VIDEO_LICENCES[ value ] !== undefined
34 }
35
36 function isVideoLanguageValid (value: any) {
37   return value === null ||
38     (typeof value === 'string' && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.LANGUAGE))
39 }
40
41 function isVideoDurationValid (value: string) {
42   return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION)
43 }
44
45 function isVideoTruncatedDescriptionValid (value: string) {
46   return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.TRUNCATED_DESCRIPTION)
47 }
48
49 function isVideoDescriptionValid (value: string) {
50   return value === null || (exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION))
51 }
52
53 function isVideoSupportValid (value: string) {
54   return value === null || (exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.SUPPORT))
55 }
56
57 function isVideoNameValid (value: string) {
58   return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
59 }
60
61 function isVideoTagValid (tag: string) {
62   return exists(tag) && validator.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG)
63 }
64
65 function isVideoTagsValid (tags: string[]) {
66   return tags === null || (
67     isArray(tags) &&
68     validator.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) &&
69     tags.every(tag => isVideoTagValid(tag))
70   )
71 }
72
73 function isVideoViewsValid (value: string) {
74   return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS)
75 }
76
77 function isVideoRatingTypeValid (value: string) {
78   return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1
79 }
80
81 const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`)
82 const videoFileTypesRegex = videoFileTypes.join('|')
83
84 function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
85   return isFileValid(files, videoFileTypesRegex, 'videofile', null)
86 }
87
88 const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
89                                           .map(v => v.replace('.', ''))
90                                           .join('|')
91 const videoImageTypesRegex = `image/(${videoImageTypes})`
92
93 function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) {
94   return isFileValid(files, videoImageTypesRegex, field, CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max, true)
95 }
96
97 function isVideoPrivacyValid (value: number) {
98   return validator.isInt(value + '') && VIDEO_PRIVACIES[ value ] !== undefined
99 }
100
101 function isScheduleVideoUpdatePrivacyValid (value: number) {
102   return validator.isInt(value + '') &&
103     (
104       value === VideoPrivacy.UNLISTED ||
105       value === VideoPrivacy.PUBLIC
106     )
107 }
108
109 function isVideoFileInfoHashValid (value: string | null | undefined) {
110   return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
111 }
112
113 function isVideoFileResolutionValid (value: string) {
114   return exists(value) && validator.isInt(value + '')
115 }
116
117 function isVideoFPSResolutionValid (value: string) {
118   return value === null || validator.isInt(value + '')
119 }
120
121 function isVideoFileSizeValid (value: string) {
122   return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE)
123 }
124
125 function isVideoMagnetUriValid (value: string) {
126   if (!exists(value)) return false
127
128   const parsed = magnetUtil.decode(value)
129   return parsed && isVideoFileInfoHashValid(parsed.infoHash)
130 }
131
132 function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: Response) {
133   // Retrieve the user who did the request
134   if (video.isOwned() === false) {
135     res.status(403)
136        .json({ error: 'Cannot manage a video of another server.' })
137        .end()
138     return false
139   }
140
141   // Check if the user can delete the video
142   // The user can delete it if he has the right
143   // Or if s/he is the video's account
144   const account = video.VideoChannel.Account
145   if (user.hasRight(right) === false && account.userId !== user.id) {
146     res.status(403)
147        .json({ error: 'Cannot manage a video of another user.' })
148        .end()
149     return false
150   }
151
152   return true
153 }
154
155 async function isVideoExist (id: string, res: Response, fetchType: 'all' | 'only-video' | 'id' | 'none' = 'all') {
156   let video: VideoModel | null
157
158   if (fetchType === 'all') {
159     video = await VideoModel.loadAndPopulateAccountAndServerAndTags(id)
160   } else if (fetchType === 'only-video') {
161     video = await VideoModel.load(id)
162   } else if (fetchType === 'id' || fetchType === 'none') {
163     video = await VideoModel.loadOnlyId(id)
164   }
165
166   if (video === null) {
167     res.status(404)
168        .json({ error: 'Video not found' })
169        .end()
170
171     return false
172   }
173
174   if (fetchType !== 'none') res.locals.video = video
175   return true
176 }
177
178 async function isVideoChannelOfAccountExist (channelId: number, user: UserModel, res: Response) {
179   if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
180     const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
181     if (videoChannel === null) {
182       res.status(400)
183          .json({ error: 'Unknown video `video channel` on this instance.' })
184          .end()
185
186       return false
187     }
188
189     res.locals.videoChannel = videoChannel
190     return true
191   }
192
193   const videoChannel = await VideoChannelModel.loadByIdAndAccount(channelId, user.Account.id)
194   if (videoChannel === null) {
195     res.status(400)
196        .json({ error: 'Unknown video `video channel` for this account.' })
197        .end()
198
199     return false
200   }
201
202   res.locals.videoChannel = videoChannel
203   return true
204 }
205
206 // ---------------------------------------------------------------------------
207
208 export {
209   isVideoCategoryValid,
210   checkUserCanManageVideo,
211   isVideoLicenceValid,
212   isVideoLanguageValid,
213   isVideoTruncatedDescriptionValid,
214   isVideoDescriptionValid,
215   isVideoFileInfoHashValid,
216   isVideoNameValid,
217   isVideoTagsValid,
218   isVideoFPSResolutionValid,
219   isScheduleVideoUpdatePrivacyValid,
220   isVideoFile,
221   isVideoMagnetUriValid,
222   isVideoStateValid,
223   isVideoViewsValid,
224   isVideoRatingTypeValid,
225   isVideoDurationValid,
226   isVideoTagValid,
227   isVideoPrivacyValid,
228   isVideoFileResolutionValid,
229   isVideoFileSizeValid,
230   isVideoExist,
231   isVideoImage,
232   isVideoChannelOfAccountExist,
233   isVideoSupportValid
234 }