Optimize SQL requests of videos AP 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 export type VideoFetchType = 'all' | 'only-video' | 'id' | 'none'
156 async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') {
157   let video: VideoModel | null
158
159   if (fetchType === 'all') {
160     video = await VideoModel.loadAndPopulateAccountAndServerAndTags(id)
161   } else if (fetchType === 'only-video') {
162     video = await VideoModel.load(id)
163   } else if (fetchType === 'id' || fetchType === 'none') {
164     video = await VideoModel.loadOnlyId(id)
165   }
166
167   if (video === null) {
168     res.status(404)
169        .json({ error: 'Video not found' })
170        .end()
171
172     return false
173   }
174
175   if (fetchType !== 'none') res.locals.video = video
176   return true
177 }
178
179 async function isVideoChannelOfAccountExist (channelId: number, user: UserModel, res: Response) {
180   if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
181     const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
182     if (videoChannel === null) {
183       res.status(400)
184          .json({ error: 'Unknown video `video channel` on this instance.' })
185          .end()
186
187       return false
188     }
189
190     res.locals.videoChannel = videoChannel
191     return true
192   }
193
194   const videoChannel = await VideoChannelModel.loadByIdAndAccount(channelId, user.Account.id)
195   if (videoChannel === null) {
196     res.status(400)
197        .json({ error: 'Unknown video `video channel` for this account.' })
198        .end()
199
200     return false
201   }
202
203   res.locals.videoChannel = videoChannel
204   return true
205 }
206
207 // ---------------------------------------------------------------------------
208
209 export {
210   isVideoCategoryValid,
211   checkUserCanManageVideo,
212   isVideoLicenceValid,
213   isVideoLanguageValid,
214   isVideoTruncatedDescriptionValid,
215   isVideoDescriptionValid,
216   isVideoFileInfoHashValid,
217   isVideoNameValid,
218   isVideoTagsValid,
219   isVideoFPSResolutionValid,
220   isScheduleVideoUpdatePrivacyValid,
221   isVideoFile,
222   isVideoMagnetUriValid,
223   isVideoStateValid,
224   isVideoViewsValid,
225   isVideoRatingTypeValid,
226   isVideoDurationValid,
227   isVideoTagValid,
228   isVideoPrivacyValid,
229   isVideoFileResolutionValid,
230   isVideoFileSizeValid,
231   isVideoExist,
232   isVideoImage,
233   isVideoChannelOfAccountExist,
234   isVideoSupportValid
235 }