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