1 import validator from 'validator'
2 import { logger } from '@server/helpers/logger'
3 import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models'
4 import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos'
5 import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
6 import { peertubeTruncate } from '../../core-utils'
7 import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
8 import { isLiveLatencyModeValid } from '../video-lives'
14 isVideoTruncatedDescriptionValid,
17 import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
19 function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
20 return isBaseActivityValid(activity, 'Update') &&
21 sanitizeAndCheckVideoTorrentObject(activity.object)
24 function isActivityPubVideoDurationValid (value: string) {
25 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
26 return exists(value) &&
27 typeof value === 'string' &&
28 value.startsWith('PT') &&
29 value.endsWith('S') &&
30 isVideoDurationValid(value.replace(/[^0-9]+/g, ''))
33 function sanitizeAndCheckVideoTorrentObject (video: any) {
34 if (!video || video.type !== 'Video') return false
36 if (!setValidRemoteTags(video)) {
37 logger.debug('Video has invalid tags', { video })
40 if (!setValidRemoteVideoUrls(video)) {
41 logger.debug('Video has invalid urls', { video })
44 if (!setRemoteVideoTruncatedContent(video)) {
45 logger.debug('Video has invalid content', { video })
48 if (!setValidAttributedTo(video)) {
49 logger.debug('Video has invalid attributedTo', { video })
52 if (!setValidRemoteCaptions(video)) {
53 logger.debug('Video has invalid captions', { video })
56 if (!setValidRemoteIcon(video)) {
57 logger.debug('Video has invalid icons', { video })
62 if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
63 if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
64 if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true
65 if (!isBooleanValid(video.commentsEnabled)) video.commentsEnabled = false
66 if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
67 if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
68 if (!isBooleanValid(video.permanentLive)) video.permanentLive = false
69 if (!isLiveLatencyModeValid(video.latencyMode)) video.latencyMode = LiveVideoLatencyMode.DEFAULT
71 return isActivityPubUrlValid(video.id) &&
72 isVideoNameValid(video.name) &&
73 isActivityPubVideoDurationValid(video.duration) &&
74 isUUIDValid(video.uuid) &&
75 (!video.category || isRemoteNumberIdentifierValid(video.category)) &&
76 (!video.licence || isRemoteNumberIdentifierValid(video.licence)) &&
77 (!video.language || isRemoteStringIdentifierValid(video.language)) &&
78 isVideoViewsValid(video.views) &&
79 isBooleanValid(video.sensitive) &&
80 isDateValid(video.published) &&
81 isDateValid(video.updated) &&
82 (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
83 (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
84 video.attributedTo.length !== 0
87 function isRemoteVideoUrlValid (url: any) {
88 return url.type === 'Link' &&
91 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.includes(url.mediaType) &&
92 isActivityPubUrlValid(url.href) &&
93 validator.isInt(url.height + '', { min: 0 }) &&
94 validator.isInt(url.size + '', { min: 0 }) &&
95 (!url.fps || validator.isInt(url.fps + '', { min: -1 }))
99 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.includes(url.mediaType) &&
100 isActivityPubUrlValid(url.href) &&
101 validator.isInt(url.height + '', { min: 0 })
105 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.includes(url.mediaType) &&
106 validator.isLength(url.href, { min: 5 }) &&
107 validator.isInt(url.height + '', { min: 0 })
111 (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
112 isActivityPubUrlValid(url.href) &&
115 isAPVideoTrackerUrlObject(url) ||
116 isAPVideoFileUrlMetadataObject(url)
119 function isAPVideoFileUrlMetadataObject (url: any): url is ActivityVideoFileMetadataUrlObject {
121 url.type === 'Link' &&
122 url.mediaType === 'application/json' &&
123 isArray(url.rel) && url.rel.includes('metadata')
126 function isAPVideoTrackerUrlObject (url: any): url is ActivityTrackerUrlObject {
127 return isArray(url.rel) &&
128 url.rel.includes('tracker') &&
129 isActivityPubUrlValid(url.href)
132 // ---------------------------------------------------------------------------
135 sanitizeAndCheckVideoTorrentUpdateActivity,
136 isRemoteStringIdentifierValid,
137 sanitizeAndCheckVideoTorrentObject,
138 isRemoteVideoUrlValid,
139 isAPVideoFileUrlMetadataObject,
140 isAPVideoTrackerUrlObject
143 // ---------------------------------------------------------------------------
145 function setValidRemoteTags (video: any) {
146 if (Array.isArray(video.tag) === false) return false
148 video.tag = video.tag.filter(t => {
149 return t.type === 'Hashtag' &&
150 isVideoTagValid(t.name)
156 function setValidRemoteCaptions (video: any) {
157 if (!video.subtitleLanguage) video.subtitleLanguage = []
159 if (Array.isArray(video.subtitleLanguage) === false) return false
161 video.subtitleLanguage = video.subtitleLanguage.filter(caption => {
162 if (!isActivityPubUrlValid(caption.url)) caption.url = null
164 return isRemoteStringIdentifierValid(caption)
170 function isRemoteNumberIdentifierValid (data: any) {
171 return validator.isInt(data.identifier, { min: 0 })
174 function isRemoteStringIdentifierValid (data: any) {
175 return typeof data.identifier === 'string'
178 function isRemoteVideoContentValid (mediaType: string, content: string) {
179 return mediaType === 'text/markdown' && isVideoTruncatedDescriptionValid(content)
182 function setValidRemoteIcon (video: any) {
183 if (video.icon && !isArray(video.icon)) video.icon = [ video.icon ]
184 if (!video.icon) video.icon = []
186 video.icon = video.icon.filter(icon => {
187 return icon.type === 'Image' &&
188 isActivityPubUrlValid(icon.url) &&
189 icon.mediaType === 'image/jpeg' &&
190 validator.isInt(icon.width + '', { min: 0 }) &&
191 validator.isInt(icon.height + '', { min: 0 })
194 return video.icon.length !== 0
197 function setValidRemoteVideoUrls (video: any) {
198 if (Array.isArray(video.url) === false) return false
200 video.url = video.url.filter(u => isRemoteVideoUrlValid(u))
205 function setRemoteVideoTruncatedContent (video: any) {
207 video.content = peertubeTruncate(video.content, { length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max })