Init commit

This commit is contained in:
ShreejitPanchal
2026-05-19 19:56:02 +08:00
commit 6601501eff
4047 changed files with 2185372 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
import validator from 'validator'
import { abusePredefinedReasonsMap } from '@peertube/peertube-core-utils'
import { AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseVideoIs } from '@peertube/peertube-models'
import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
import { exists, isArray } from './misc.js'
const ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES
const ABUSE_MESSAGES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSE_MESSAGES
function isAbuseReasonValid (value: string) {
return exists(value) && validator.default.isLength(value, ABUSES_CONSTRAINTS_FIELDS.REASON)
}
function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) {
return exists(value) && value in abusePredefinedReasonsMap
}
function isAbuseFilterValid (value: AbuseFilter) {
return value === 'video' || value === 'comment' || value === 'account'
}
function areAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) {
return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap)
}
function isAbuseTimestampValid (value: number) {
return value === null || (exists(value) && validator.default.isInt('' + value, { min: 0 }))
}
function isAbuseTimestampCoherent (endAt: number, { req }) {
const startAt = (req.body as AbuseCreate).video.startAt
return exists(startAt) && endAt > startAt
}
function isAbuseModerationCommentValid (value: string) {
return exists(value) && validator.default.isLength(value, ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
}
function isAbuseStateValid (value: string) {
return exists(value) && ABUSE_STATES[value] !== undefined
}
function isAbuseVideoIsValid (value: AbuseVideoIs) {
return exists(value) && (
value === 'deleted' ||
value === 'blacklisted'
)
}
function isAbuseMessageValid (value: string) {
return exists(value) && validator.default.isLength(value, ABUSE_MESSAGES_CONSTRAINTS_FIELDS.MESSAGE)
}
// ---------------------------------------------------------------------------
export {
isAbuseReasonValid,
isAbuseFilterValid,
isAbusePredefinedReasonValid,
isAbuseMessageValid,
areAbusePredefinedReasonsValid,
isAbuseTimestampValid,
isAbuseTimestampCoherent,
isAbuseModerationCommentValid,
isAbuseStateValid,
isAbuseVideoIsValid
}

View File

@@ -0,0 +1,22 @@
import { isUserDescriptionValid, isUserUsernameValid } from './users.js'
import { exists } from './misc.js'
function isAccountNameValid (value: string) {
return isUserUsernameValid(value)
}
function isAccountIdValid (value: string) {
return exists(value)
}
function isAccountDescriptionValid (value: string) {
return isUserDescriptionValid(value)
}
// ---------------------------------------------------------------------------
export {
isAccountIdValid,
isAccountDescriptionValid,
isAccountNameValid
}

View File

@@ -0,0 +1,150 @@
import { Activity, ActivityType } from '@peertube/peertube-models'
import validator from 'validator'
import { isAbuseReasonValid } from '../abuses.js'
import { exists } from '../misc.js'
import { sanitizeAndCheckActorObject } from './actor.js'
import { isCacheFileObjectValid } from './cache-file.js'
import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc.js'
import { sanitizeAndCheckPlayerSettingsObject } from './player-settings.js'
import { isPlaylistObjectValid } from './playlist.js'
import { sanitizeAndCheckVideoCommentObject } from './video-comments.js'
import { sanitizeAndCheckVideoTorrentObject } from './videos.js'
import { isWatchActionObjectValid } from './watch-action.js'
export function isRootActivityValid (activity: any) {
return isCollection(activity) || isActivity(activity)
}
function isCollection (activity: any) {
return (activity.type === 'Collection' || activity.type === 'OrderedCollection') &&
validator.default.isInt(activity.totalItems, { min: 0 }) &&
Array.isArray(activity.items)
}
function isActivity (activity: any) {
return isActivityPubUrlValid(activity.id) &&
exists(activity.actor) &&
(isActivityPubUrlValid(activity.actor) || isActivityPubUrlValid(activity.actor.id))
}
// ---------------------------------------------------------------------------
const activityCheckers: { [P in ActivityType]: (activity: Activity) => boolean } = {
Create: isCreateActivityValid,
Update: isUpdateActivityValid,
Delete: isDeleteActivityValid,
Follow: isFollowActivityValid,
Accept: isAcceptActivityValid,
Reject: isRejectActivityValid,
Announce: isAnnounceActivityValid,
Undo: isUndoActivityValid,
Like: isLikeActivityValid,
View: isViewActivityValid,
Flag: isFlagActivityValid,
Dislike: isDislikeActivityValid,
ApproveReply: isApproveReplyActivityValid,
RejectReply: isRejectReplyActivityValid
}
export function isActivityValid (activity: any) {
const checker = activityCheckers[activity.type]
// Unknown activity type
if (!checker) return false
return checker(activity)
}
export function isFlagActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Flag') &&
isAbuseReasonValid(activity.content) &&
isActivityPubUrlValid(activity.object)
}
export function isLikeActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Like') &&
isObjectValid(activity.object)
}
export function isDislikeActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Dislike') &&
isObjectValid(activity.object)
}
export function isAnnounceActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Announce') &&
isObjectValid(activity.object)
}
export function isViewActivityValid (activity: any) {
return isBaseActivityValid(activity, 'View') &&
isActivityPubUrlValid(activity.actor) &&
isActivityPubUrlValid(activity.object)
}
export function isCreateActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
(
isViewActivityValid(activity.object) ||
isDislikeActivityValid(activity.object) ||
isFlagActivityValid(activity.object) ||
isPlaylistObjectValid(activity.object) ||
isWatchActionObjectValid(activity.object) ||
isCacheFileObjectValid(activity.object) ||
sanitizeAndCheckVideoCommentObject(activity.object) ||
sanitizeAndCheckVideoTorrentObject(activity.object)
)
}
export function isUpdateActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Update') &&
(
isCacheFileObjectValid(activity.object) ||
isPlaylistObjectValid(activity.object) ||
sanitizeAndCheckVideoTorrentObject(activity.object) ||
sanitizeAndCheckActorObject(activity.object) ||
sanitizeAndCheckPlayerSettingsObject(activity.object, 'video') ||
sanitizeAndCheckPlayerSettingsObject(activity.object, 'channel')
)
}
export function isDeleteActivityValid (activity: any) {
// We don't really check objects
return isBaseActivityValid(activity, 'Delete') &&
isObjectValid(activity.object)
}
export function isFollowActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Follow') &&
isObjectValid(activity.object)
}
export function isAcceptActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Accept')
}
export function isRejectActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Reject')
}
export function isUndoActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Undo') &&
(
isFollowActivityValid(activity.object) ||
isLikeActivityValid(activity.object) ||
isDislikeActivityValid(activity.object) ||
isAnnounceActivityValid(activity.object) ||
isCreateActivityValid(activity.object)
)
}
export function isApproveReplyActivityValid (activity: any) {
return isBaseActivityValid(activity, 'ApproveReply') &&
isActivityPubUrlValid(activity.object) &&
isActivityPubUrlValid(activity.inReplyTo)
}
export function isRejectReplyActivityValid (activity: any) {
return isBaseActivityValid(activity, 'RejectReply') &&
isActivityPubUrlValid(activity.object) &&
isActivityPubUrlValid(activity.inReplyTo)
}

View File

@@ -0,0 +1,132 @@
import { arrayify } from '@peertube/peertube-core-utils'
import { ActivityPubActor } from '@peertube/peertube-models'
import { peertubeTruncate } from '@server/helpers/core-utils.js'
import validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
import { exists, isArray, isDateValid } from '../misc.js'
import { isHostValid } from '../servers.js'
import { isActivityPubHTMLUrlValid, isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc.js'
export function isActorEndpointsObjectValid (endpointObject: any) {
if (endpointObject?.sharedInbox) {
return isActivityPubUrlValid(endpointObject.sharedInbox)
}
// Shared inbox is optional
return true
}
export function isActorPublicKeyObjectValid (publicKeyObject: any) {
return isActivityPubUrlValid(publicKeyObject.id) &&
isActivityPubUrlValid(publicKeyObject.owner) &&
isActorPublicKeyValid(publicKeyObject.publicKeyPem)
}
const actorTypes = new Set([ 'Person', 'Application', 'Group', 'Service', 'Organization' ])
export function isActorTypeValid (type: string) {
return actorTypes.has(type)
}
export function isActorPublicKeyValid (publicKey: string) {
return exists(publicKey) &&
typeof publicKey === 'string' &&
publicKey.startsWith('-----BEGIN PUBLIC KEY-----') &&
publicKey.includes('-----END PUBLIC KEY-----') &&
validator.default.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY)
}
export const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.:]'
const actorNameRegExp = new RegExp(`^${actorNameAlphabet}+$`)
export function isActorPreferredUsernameValid (preferredUsername: string) {
return exists(preferredUsername) && validator.default.matches(preferredUsername, actorNameRegExp)
}
export function isActorPrivateKeyValid (privateKey: string) {
return exists(privateKey) &&
typeof privateKey === 'string' &&
(privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') || privateKey.startsWith('-----BEGIN PRIVATE KEY-----')) &&
// Sometimes there is a \n at the end, so just assert the string contains the end mark
(privateKey.includes('-----END RSA PRIVATE KEY-----') || privateKey.includes('-----END PRIVATE KEY-----')) &&
validator.default.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY)
}
export function isActorFollowingCountValid (value: string) {
return exists(value) && validator.default.isInt('' + value, { min: 0 })
}
export function isActorFollowersCountValid (value: string) {
return exists(value) && validator.default.isInt('' + value, { min: 0 })
}
export function isActorDeleteActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Delete')
}
export function sanitizeAndCheckActorObject (actor: ActivityPubActor) {
if (!isActorTypeValid(actor.type)) return false
normalizeActor(actor)
return exists(actor) &&
isActivityPubUrlValid(actor.id) &&
isActivityPubUrlValid(actor.inbox) &&
isActorPreferredUsernameValid(actor.preferredUsername) &&
isActorPublicKeyObjectValid(actor.publicKey) &&
isActorEndpointsObjectValid(actor.endpoints) &&
(!actor.outbox || isActivityPubUrlValid(actor.outbox)) &&
(!actor.following || isActivityPubUrlValid(actor.following)) &&
(!actor.followers || isActivityPubUrlValid(actor.followers)) &&
// If this is a group (a channel), it should be attributed to an account
// In PeerTube we use this to attach a video channel to a specific account
(actor.type !== 'Group' || actor.attributedTo.length !== 0)
}
export function isValidActorHandle (handle: string) {
if (!exists(handle)) return false
const parts = handle.split('@')
if (parts.length !== 2) return false
return isHostValid(parts[1])
}
export function areValidActorHandles (handles: string[]) {
return isArray(handles) && handles.every(h => isValidActorHandle(h))
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
function normalizeActor (actor: ActivityPubActor) {
if (!actor) return
setValidUrls(actor)
setValidAttributedTo(actor)
setValidDescription(actor)
if (!isDateValid(actor.published)) actor.published = undefined
if (actor.summary && typeof actor.summary === 'string') {
actor.summary = peertubeTruncate(actor.summary, { length: CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max })
if (actor.summary.length < CONSTRAINTS_FIELDS.USERS.DESCRIPTION.min) {
actor.summary = null
}
}
}
function setValidDescription (actor: ActivityPubActor) {
if (!actor.summary) actor.summary = null
}
function setValidUrls (actor: any) {
if (!actor.url) {
actor.url = []
return
}
actor.url = arrayify(actor.url)
.filter(u => isActivityPubHTMLUrlValid(u))
}

View File

@@ -0,0 +1,24 @@
import { CacheFileObject } from '@peertube/peertube-models'
import { isDateValid } from '../misc.js'
import { isActivityPubUrlValid } from './misc.js'
import { isRemoteVideoUrlValid } from './videos.js'
export function isCacheFileObjectValid (object: CacheFileObject) {
if (object?.type !== 'CacheFile') return false
return (!object.expires || isDateValid(object.expires)) &&
isActivityPubUrlValid(object.object) &&
(isRedundancyUrlVideoValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
}
// ---------------------------------------------------------------------------
function isPlaylistRedundancyUrlValid (url: any) {
return url.type === 'Link' &&
(url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href)
}
function isRedundancyUrlVideoValid (url: any) {
return isRemoteVideoUrlValid(url)
}

View File

@@ -0,0 +1,75 @@
import { ActivityHtmlUrlObject } from '@peertube/peertube-models'
import { CONFIG } from '@server/initializers/config.js'
import validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
import { exists } from '../misc.js'
export function isUrlValid (url: string) {
const isURLOptions = {
require_host: true,
require_tld: true,
require_protocol: true,
require_valid_protocol: true,
protocols: [ 'http', 'https' ]
}
// We validate 'localhost', so we don't have the top level domain
if (CONFIG.WEBSERVER.HOSTNAME === 'localhost' || CONFIG.WEBSERVER.HOSTNAME === '127.0.0.1') {
isURLOptions.require_tld = false
}
return exists(url) && validator.default.isURL('' + url, isURLOptions)
}
export function isActivityPubUrlValid (url: string) {
return isUrlValid(url) && validator.default.isLength('' + url, CONSTRAINTS_FIELDS.ACTORS.URL)
}
export function isBaseActivityValid (activity: any, type: string) {
return activity.type === type &&
isActivityPubUrlValid(activity.id) &&
isObjectValid(activity.actor) &&
isUrlCollectionValid(activity.to) &&
isUrlCollectionValid(activity.cc)
}
export function isUrlCollectionValid (collection: any) {
return collection === undefined ||
(typeof collection === 'string' && isActivityPubUrlValid(collection)) ||
(Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t)))
}
export function isObjectValid (object: any) {
return exists(object) &&
(
isActivityPubUrlValid(object) || isActivityPubUrlValid(object.id)
)
}
export function isActivityPubHTMLUrlValid (url: ActivityHtmlUrlObject) {
return url?.type === 'Link' &&
url.mediaType === 'text/html' &&
isActivityPubUrlValid(url.href)
}
export function setValidAttributedTo (obj: any) {
if (Array.isArray(obj.attributedTo) === false) {
obj.attributedTo = []
return true
}
obj.attributedTo = obj.attributedTo.filter(a => {
return isActivityPubUrlValid(a) ||
((a.type === 'Group' || a.type === 'Person') && isActivityPubUrlValid(a.id))
})
return true
}
export function isActivityPubVideoDurationValid (value: string) {
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
return exists(value) &&
typeof value === 'string' &&
value.startsWith('PT') &&
value.endsWith('S')
}

View File

@@ -0,0 +1,13 @@
import { PlayerSettingsObject } from '@peertube/peertube-models'
import { isPlayerChannelThemeSettingValid, isPlayerVideoThemeSettingValid } from '../player-settings.js'
import { isActivityPubUrlValid } from './misc.js'
export function sanitizeAndCheckPlayerSettingsObject (settings: PlayerSettingsObject, target: 'video' | 'channel') {
if (!settings) return false
if (settings.type !== 'PlayerSettings') return false
if (target === 'video' && !isPlayerVideoThemeSettingValid(settings.theme)) return false
if (target === 'channel' && !isPlayerChannelThemeSettingValid(settings.theme)) return false
return isActivityPubUrlValid(settings.id)
}

View File

@@ -0,0 +1,22 @@
import { PlaylistElementObject, PlaylistObject } from '@peertube/peertube-models'
import validator from 'validator'
import { exists, isDateValid, isUUIDValid } from '../misc.js'
import { isVideoPlaylistNameValid } from '../video-playlists.js'
import { isActivityPubUrlValid } from './misc.js'
export function isPlaylistObjectValid (object: PlaylistObject) {
if (object?.type !== 'Playlist') return false
return validator.default.isInt(object.totalItems + '') &&
isVideoPlaylistNameValid(object.name) &&
isUUIDValid(object.uuid) &&
isDateValid(object.published) &&
isDateValid(object.updated)
}
export function isPlaylistElementObjectValid (object: PlaylistElementObject) {
return exists(object) &&
object.type === 'PlaylistElement' &&
validator.default.isInt(object.position + '') &&
isActivityPubUrlValid(object.url)
}

View File

@@ -0,0 +1,22 @@
import { exists } from '../misc.js'
import { isActivityPubUrlValid } from './misc.js'
function isSignatureTypeValid (signatureType: string) {
return exists(signatureType) && signatureType === 'RsaSignature2017'
}
function isSignatureCreatorValid (signatureCreator: string) {
return exists(signatureCreator) && isActivityPubUrlValid(signatureCreator)
}
function isSignatureValueValid (signatureValue: string) {
return exists(signatureValue) && signatureValue.length > 0
}
// ---------------------------------------------------------------------------
export {
isSignatureTypeValid,
isSignatureCreatorValid,
isSignatureValueValid
}

View File

@@ -0,0 +1,15 @@
import { isArray } from '../misc.js'
import { isVideoChapterTitleValid, isVideoChapterTimecodeValid } from '../video-chapters.js'
import { isActivityPubUrlValid } from './misc.js'
import { VideoChaptersObject } from '@peertube/peertube-models'
export function isVideoChaptersObjectValid (object: VideoChaptersObject) {
if (!object) return false
if (!isActivityPubUrlValid(object.id)) return false
if (!isArray(object.hasPart)) return false
return object.hasPart.every(part => {
return isVideoChapterTitleValid(part.name) && isVideoChapterTimecodeValid(part.startOffset)
})
}

View File

@@ -0,0 +1,57 @@
import { ActivityTombstoneObject, VideoCommentObject } from '@peertube/peertube-models'
import { hasAPPublic } from '@server/helpers/activity-pub-utils.js'
import validator from 'validator'
import { exists, isDateValid } from '../misc.js'
import { isActivityPubUrlValid } from './misc.js'
function sanitizeAndCheckVideoCommentObject (comment: VideoCommentObject | ActivityTombstoneObject) {
if (!comment) return false
if (!isCommentTypeValid(comment)) return false
normalizeComment(comment)
if (comment.type === 'Tombstone') {
return isActivityPubUrlValid(comment.id) &&
isDateValid(comment.published) &&
isDateValid(comment.deleted) &&
isActivityPubUrlValid(comment.url)
}
return isActivityPubUrlValid(comment.id) &&
isCommentContentValid(comment.content) &&
isActivityPubUrlValid(comment.inReplyTo) &&
isDateValid(comment.published) &&
isActivityPubUrlValid(comment.url) &&
(!exists(comment.replyApproval) || isActivityPubUrlValid(comment.replyApproval)) &&
(hasAPPublic(comment.to) || hasAPPublic(comment.cc)) // Only accept public or unlisted comments
}
// ---------------------------------------------------------------------------
export {
sanitizeAndCheckVideoCommentObject
}
// ---------------------------------------------------------------------------
function isCommentContentValid (content: any) {
return exists(content) && validator.default.isLength('' + content, { min: 1 })
}
function normalizeComment (comment: any) {
if (!comment) return
if (typeof comment.url !== 'string') {
if (typeof comment.url === 'object') comment.url = comment.url.href || comment.url.url
else comment.url = comment.id
}
}
function isCommentTypeValid (comment: any): boolean {
if (comment.type === 'Note') return true
if (comment.type === 'Tombstone' && comment.formerType === 'Note') return true
return false
}

View File

@@ -0,0 +1,279 @@
import { arrayify } from '@peertube/peertube-core-utils'
import {
ActivityCaptionUrlObject,
ActivityPubStoryboard,
ActivityTrackerUrlObject,
ActivityVideoFileMetadataUrlObject,
LiveVideoLatencyMode,
VideoCommentPolicy,
VideoObject,
VideoState
} from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { spdxToPeertubeLicence } from '@server/helpers/video.js'
import validator from 'validator'
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
import { peertubeTruncate } from '../../core-utils.js'
import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc.js'
import { isLiveLatencyModeValid } from '../video-lives.js'
import {
isVideoCommentsPolicyValid,
isVideoDescriptionValid,
isVideoDurationValid,
isVideoNameValid,
isVideoStateValid,
isVideoTagValid,
isVideoViewsValid
} from '../videos.js'
import { isActivityPubUrlValid, isActivityPubVideoDurationValid, isBaseActivityValid, setValidAttributedTo } from './misc.js'
export function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
return isBaseActivityValid(activity, 'Update') &&
sanitizeAndCheckVideoTorrentObject(activity.object)
}
export function sanitizeAndCheckVideoTorrentObject (video: VideoObject) {
if (video?.type !== 'Video') return false
const fail = (field: string) => {
logger.debug(`Video field is not valid to PeerTube: ${field}`, { video })
return false
}
if (!setValidRemoteTags(video)) return fail('tags')
if (!setValidRemoteVideoUrls(video)) return fail('urls')
if (!setRemoteVideoContent(video)) return fail('content')
if (!setValidAttributedTo(video)) return fail('attributedTo')
if (!setValidRemoteCaptions(video)) return fail('captions')
if (!setValidRemoteIcon(video)) return fail('icons')
if (!setValidStoryboard(video)) return fail('preview (storyboard)')
if (!setValidLicence(video)) return fail('licence')
// Default attributes
if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true
if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
if (!isBooleanValid(video.permanentLive)) video.permanentLive = false
if (!isBooleanValid(video.sensitive)) video.sensitive = false
if (!isLiveLatencyModeValid(video.latencyMode)) video.latencyMode = LiveVideoLatencyMode.DEFAULT
if (video.commentsPolicy) {
if (!isVideoCommentsPolicyValid(video.commentsPolicy)) {
video.commentsPolicy = VideoCommentPolicy.DISABLED
}
} else {
video.commentsPolicy = VideoCommentPolicy.DISABLED
}
if (!isActivityPubUrlValid(video.id)) return fail('id')
if (!isVideoNameValid(video.name)) return fail('name')
if (!isActivityPubVideoDurationValid(video.duration)) return fail('duration format')
if (!isVideoDurationValid(video.duration.replace(/[^0-9]+/g, ''))) return fail('duration')
if (!isUUIDValid(video.uuid)) return fail('uuid')
if (exists(video.category) && !isRemoteNumberIdentifierValid(video.category)) return fail('category')
if (exists(video.language) && !isRemoteStringIdentifierValid(video.language)) return fail('language')
if (!isVideoViewsValid(video.views)) return fail('views')
if (!isDateValid(video.published)) return fail('published')
if (!isDateValid(video.updated)) return fail('updated')
if (exists(video.originallyPublishedAt) && !isDateValid(video.originallyPublishedAt)) return fail('originallyPublishedAt')
if (exists(video.uploadDate) && !isDateValid(video.uploadDate)) return fail('uploadDate')
if (exists(video.content) && !isRemoteVideoContentValid(video.mediaType, video.content)) return fail('mediaType/content')
if (video.attributedTo.length === 0) return fail('attributedTo')
return true
}
export function isRemoteVideoUrlValid (url: any) {
return url.type === 'Link' &&
// Video file link
(
MIMETYPES.AP_VIDEO.MIMETYPE_EXT[url.mediaType] &&
isActivityPubUrlValid(url.href) &&
validator.default.isInt(url.height + '', { min: 0 }) &&
validator.default.isInt(url.size + '', { min: 0 }) &&
(!url.fps || validator.default.isInt(url.fps + '', { min: -1 }))
) ||
// Torrent link
(
MIMETYPES.AP_TORRENT.MIMETYPE_EXT[url.mediaType] &&
isActivityPubUrlValid(url.href) &&
validator.default.isInt(url.height + '', { min: 0 })
) ||
// Magnet link
(
MIMETYPES.AP_MAGNET.MIMETYPE_EXT[url.mediaType] &&
validator.default.isLength(url.href, { min: 5 }) &&
validator.default.isInt(url.height + '', { min: 0 })
) ||
// HLS playlist link
(
(url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href) &&
isArray(url.tag)
) ||
isAPVideoTrackerUrlObject(url) ||
isAPVideoFileUrlMetadataObject(url)
}
export function isAPVideoFileUrlMetadataObject (url: any): url is ActivityVideoFileMetadataUrlObject {
return url?.type === 'Link' &&
url.mediaType === 'application/json' &&
isArray(url.rel) && url.rel.includes('metadata')
}
export function isAPVideoTrackerUrlObject (url: any): url is ActivityTrackerUrlObject {
return isArray(url.rel) &&
url.rel.includes('tracker') &&
isActivityPubUrlValid(url.href)
}
export function setAPCaptionUrlObject (url: any): url is ActivityCaptionUrlObject {
if (url?.type !== 'Link') return false
if (!isActivityPubUrlValid(url.href)) return false
if (!url.mediaType && url.href.endsWith('.vtt')) {
url.mediaType = 'text/vtt'
}
return url.mediaType === 'text/vtt' || url.mediaType === 'application/x-mpegURL'
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
function setValidRemoteTags (video: VideoObject) {
if (Array.isArray(video.tag) === false) video.tag = []
video.tag = video.tag.filter(t => {
return (t.type === 'Hashtag' && isVideoTagValid(t.name)) ||
(t.type === 'SensitiveTag' && !!t.name)
})
return true
}
function setValidRemoteCaptions (video: VideoObject) {
if (!video.subtitleLanguage) video.subtitleLanguage = []
if (Array.isArray(video.subtitleLanguage) === false) return false
video.subtitleLanguage = video.subtitleLanguage.filter(caption => {
if (typeof caption.url === 'string') {
if (isActivityPubUrlValid(caption.url)) {
caption.url = [
{
type: 'Link',
href: caption.url,
mediaType: 'text/vtt'
}
]
} else {
caption.url = []
}
} else {
caption.url = arrayify(caption.url).filter(u => setAPCaptionUrlObject(u))
}
return caption.url.length > 0 && isRemoteStringIdentifierValid(caption)
})
return true
}
function isRemoteNumberIdentifierValid (data: any) {
return validator.default.isInt(data.identifier, { min: 0 })
}
function isRemoteStringIdentifierValid (data: any) {
return typeof data.identifier === 'string'
}
function isRemoteVideoContentValid (mediaType: string, content: string) {
return (mediaType === 'text/markdown' || mediaType === 'text/html') && isVideoDescriptionValid(content)
}
function setValidRemoteIcon (video: any) {
if (video.icon && !isArray(video.icon)) video.icon = [ video.icon ]
if (!video.icon) video.icon = []
video.icon = video.icon.filter(icon => {
return icon.type === 'Image' &&
isActivityPubUrlValid(icon.url) &&
icon.mediaType === 'image/jpeg' &&
validator.default.isInt(icon.width + '', { min: 0 }) &&
validator.default.isInt(icon.height + '', { min: 0 })
})
return video.icon.length !== 0
}
function setValidRemoteVideoUrls (video: any) {
if (Array.isArray(video.url) === false) return false
video.url = video.url.filter(u => isRemoteVideoUrlValid(u))
return true
}
function setRemoteVideoContent (video: VideoObject) {
if (video.content) {
video.content = peertubeTruncate(video.content, { length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max })
}
return true
}
function setValidLicence (video: VideoObject) {
if (!exists(video.licence)) return true
if (validator.default.isInt(video.licence.identifier)) return isRemoteNumberIdentifierValid(video.licence)
const spdx = spdxToPeertubeLicence(video.licence.identifier)
video.licence.identifier = spdx
? spdx + ''
: undefined
return true
}
function setValidStoryboard (video: VideoObject) {
if (!video.preview) return true
if (!Array.isArray(video.preview)) return false
video.preview = video.preview.filter(p => isStorybordValid(p))
return true
}
function isStorybordValid (preview: ActivityPubStoryboard) {
if (!preview) return false
if (
preview.type !== 'Image' ||
!isArray(preview.rel) ||
!preview.rel.includes('storyboard')
) {
return false
}
preview.url = preview.url.filter(u => {
return u.mediaType === 'image/jpeg' &&
isActivityPubUrlValid(u.href) &&
validator.default.isInt(u.width + '', { min: 0 }) &&
validator.default.isInt(u.height + '', { min: 0 }) &&
validator.default.isInt(u.tileWidth + '', { min: 0 }) &&
validator.default.isInt(u.tileHeight + '', { min: 0 }) &&
isActivityPubVideoDurationValid(u.tileDuration)
})
return preview.url.length !== 0
}

View File

@@ -0,0 +1,41 @@
import { WatchActionObject } from '@peertube/peertube-models'
import { isDateValid, isUUIDValid } from '../misc.js'
import { isVideoTimeValid } from '../video-view.js'
import { isActivityPubVideoDurationValid, isObjectValid } from './misc.js'
function isWatchActionObjectValid (action: WatchActionObject) {
if (action?.type !== 'WatchAction') return false
return isObjectValid(action.id) &&
isActivityPubVideoDurationValid(action.duration) &&
isDateValid(action.startTime) &&
isDateValid(action.endTime) &&
isLocationValid(action.location) &&
isUUIDValid(action.uuid) &&
isObjectValid(action.object) &&
areWatchSectionsValid(action.watchSections)
}
// ---------------------------------------------------------------------------
export {
isWatchActionObjectValid
}
// ---------------------------------------------------------------------------
function isLocationValid (location: any) {
if (!location) return true
if (typeof location !== 'object') return false
if (location.addressCountry && typeof location.addressCountry !== 'string') return false
if (location.addressRegion && typeof location.addressRegion !== 'string') return false
return true
}
function areWatchSectionsValid (sections: WatchActionObject['watchSections']) {
return Array.isArray(sections) && sections.every(s => {
return isVideoTimeValid(s.startTimestamp) && isVideoTimeValid(s.endTimestamp)
})
}

View File

@@ -0,0 +1,17 @@
import { UploadFilesForCheck } from 'express'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
import { isFileValid } from './misc.js'
const imageMimeTypes = CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
.map(v => v.replace('.', ''))
.join('|')
const imageMimeTypesRegex = `image/(${imageMimeTypes})`
export function isActorImageFile (files: UploadFilesForCheck, fieldname: string) {
return isFileValid({
files,
mimeTypeRegex: imageMimeTypesRegex,
field: fieldname,
maxSize: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
})
}

View File

@@ -0,0 +1,53 @@
import { t } from '../i18n.js'
export function getBrowseVideosDefaultSortError (value: string, enabledTrendingAlgorithms: string[], language?: string) {
const availableOptions = [ '-publishedAt', '-originallyPublishedAt', 'name', '-trending', '-hot', '-likes', '-views' ]
if (availableOptions.includes(value) === false) {
const options = availableOptions.join(' or ')
// Only use `t` if language is provided, because i18n might not be initialized yet and can return `undefined`
if (language) {
return t('Browse videos default sort should be {options}, instead of {value}', language, { options, value })
}
return `Browse videos default sort should be ${options}, instead of ${value}`
}
const trendingSortAlgorithmMap = new Map<string, string>([
[ '-trending', 'most-viewed' ],
[ '-hot', 'hot' ],
[ '-likes', 'most-liked' ]
])
const currentTrendingSortAlgorithm = trendingSortAlgorithmMap.get(value)
if (currentTrendingSortAlgorithm && enabledTrendingAlgorithms.includes(currentTrendingSortAlgorithm) === false) {
if (language) {
return t(
'Trending videos algorithm {currentTrendingSortAlgorithm} should be enabled if browse videos default sort is {value}',
language,
{ currentTrendingSortAlgorithm, value }
)
}
return `Trending videos algorithm ${currentTrendingSortAlgorithm} should be enabled if browse videos default sort is ${value}`
}
return null
}
export function getBrowseVideosDefaultScopeError (value: string, language?: string) {
const availableOptions = [ 'local', 'federated' ]
if (availableOptions.includes(value) === false) {
const options = availableOptions.join(' or ')
if (language) {
return t('Browse videos default scope should be {options}, instead of {value}', language, { options, value })
}
return `Browse videos default scope should be ${options}, instead of ${value}`
}
return null
}

View File

@@ -0,0 +1,5 @@
import { BulkRemoveCommentsOfBody } from '@peertube/peertube-models'
export function isBulkRemoveCommentsOfScopeValid (value: BulkRemoveCommentsOfBody['scope']) {
return value === 'my-videos' || value === 'instance' || value === 'my-videos-and-collaborations'
}

View File

@@ -0,0 +1,23 @@
import { LogoType } from '@peertube/peertube-models'
import { CONSTRAINTS_FIELDS, MIMETYPES } from '@server/initializers/constants.js'
import { UploadFilesForCheck } from 'express'
import { isFileValid } from './misc.js'
const logoTypes = new Set<LogoType>([ 'favicon', 'header-square', 'header-wide', 'opengraph' ])
export function isConfigLogoTypeValid (value: LogoType) {
return logoTypes.has(value)
}
const imageMimeTypesRegex = Object.keys(MIMETYPES.LOGO_IMAGE.MIMETYPE_EXT)
.map(mimeType => `(${mimeType.replace('+', '\\+')})`)
.join('|')
export function isLogoImageFile (files: UploadFilesForCheck, fieldname: string) {
return isFileValid({
files,
mimeTypeRegex: imageMimeTypesRegex,
field: fieldname,
maxSize: CONSTRAINTS_FIELDS.LOGO.IMAGE.FILE_SIZE.max
})
}

View File

@@ -0,0 +1,23 @@
import { exists } from './misc.js'
function isValidRSSFeed (value: string) {
if (!exists(value)) return false
const feedExtensions = [
'xml',
'json',
'json1',
'rss',
'rss2',
'atom',
'atom1'
]
return feedExtensions.includes(value)
}
// ---------------------------------------------------------------------------
export {
isValidRSSFeed
}

View File

@@ -0,0 +1,30 @@
import { exists, isArray } from './misc.js'
import { FollowState } from '@peertube/peertube-models'
function isFollowStateValid (value: FollowState) {
if (!exists(value)) return false
return value === 'pending' || value === 'accepted' || value === 'rejected'
}
function isRemoteHandleValid (value: string) {
if (!exists(value)) return false
if (typeof value !== 'string') return false
return value.includes('@')
}
function isEachUniqueHandleValid (handles: string[]) {
return isArray(handles) &&
handles.every(handle => {
return isRemoteHandleValid(handle) && handles.indexOf(handle) === handles.lastIndexOf(handle)
})
}
// ---------------------------------------------------------------------------
export {
isFollowStateValid,
isRemoteHandleValid,
isEachUniqueHandleValid
}

View File

@@ -0,0 +1,21 @@
import { JobState } from '@peertube/peertube-models'
import { jobTypes } from '@server/lib/job-queue/job-queue.js'
import { exists } from './misc.js'
const jobStates = new Set<JobState>([ 'active', 'completed', 'failed', 'waiting', 'delayed', 'paused', 'waiting-children', 'prioritized' ])
function isValidJobState (value: JobState) {
return exists(value) && jobStates.has(value)
}
function isValidJobType (value: any) {
return exists(value) && jobTypes.includes(value)
}
// ---------------------------------------------------------------------------
export {
jobStates,
isValidJobState,
isValidJobType
}

View File

@@ -0,0 +1,42 @@
import validator from 'validator'
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
import { ClientLogLevel, ServerLogLevel } from '@peertube/peertube-models'
import { exists } from './misc.js'
const serverLogLevels = new Set<ServerLogLevel>([ 'debug', 'info', 'warn', 'error' ])
const clientLogLevels = new Set<ClientLogLevel>([ 'warn', 'error' ])
function isValidLogLevel (value: any) {
return exists(value) && serverLogLevels.has(value)
}
function isValidClientLogMessage (value: any) {
return typeof value === 'string' && validator.default.isLength(value, CONSTRAINTS_FIELDS.LOGS.CLIENT_MESSAGE)
}
function isValidClientLogLevel (value: any) {
return exists(value) && clientLogLevels.has(value)
}
function isValidClientLogStackTrace (value: any) {
return typeof value === 'string' && validator.default.isLength(value, CONSTRAINTS_FIELDS.LOGS.CLIENT_STACK_TRACE)
}
function isValidClientLogMeta (value: any) {
return typeof value === 'string' && validator.default.isLength(value, CONSTRAINTS_FIELDS.LOGS.CLIENT_META)
}
function isValidClientLogUserAgent (value: any) {
return typeof value === 'string' && validator.default.isLength(value, CONSTRAINTS_FIELDS.LOGS.CLIENT_USER_AGENT)
}
// ---------------------------------------------------------------------------
export {
isValidLogLevel,
isValidClientLogMessage,
isValidClientLogStackTrace,
isValidClientLogMeta,
isValidClientLogLevel,
isValidClientLogUserAgent
}

View File

@@ -0,0 +1,3 @@
export function isValidPlayerMode (value: any) {
return value === 'web-video' || value === 'p2p-media-loader'
}

View File

@@ -0,0 +1,196 @@
import { isShortUUID, shortToUUID } from '@peertube/peertube-node-utils'
import { UploadFilesForCheck } from 'express'
import 'multer'
import { sep } from 'path'
import validator from 'validator'
export function exists (value: any) {
return value !== undefined && value !== null
}
export function isSafePath (p: string) {
return exists(p) &&
(p + '').split(sep).every(part => {
return [ '..' ].includes(part) === false
})
}
export function isSafeFilename (filename: string, extension?: string) {
const regex = extension
? new RegExp(`^[a-z0-9-]+\\.${extension}$`)
: new RegExp(`^[a-z0-9-]+\\.[a-z0-9]{1,8}$`)
return typeof filename === 'string' && !!filename.match(regex)
}
export function isSafePeerTubeFilenameWithoutExtension (filename: string) {
return filename.match(/^[a-z0-9-]+$/)
}
// ---------------------------------------------------------------------------
export function isArray (value: any): value is any[] {
return Array.isArray(value)
}
export function isNotEmptyIntArray (value: any) {
return Array.isArray(value) && value.every(v => validator.default.isInt('' + v)) && value.length !== 0
}
export function isNotEmptyStringArray (value: any) {
return Array.isArray(value) && value.every(v => typeof v === 'string' && v.length !== 0) && value.length !== 0
}
export function hasArrayLength (value: unknown[], options: { min?: number, max?: number }) {
if (options.min !== undefined && value.length < options.min) return false
if (options.max !== undefined && value.length > options.max) return false
return true
}
export function isArrayOf (value: any, validator: (value: any) => boolean) {
return isArray(value) && value.every(v => validator(v))
}
// ---------------------------------------------------------------------------
export function isDateValid (value: string) {
return exists(value) && validator.default.isISO8601(value)
}
export function isIdValid (value: string) {
return exists(value) && validator.default.isInt('' + value)
}
export function isUUIDValid (value: string) {
return exists(value) && validator.default.isUUID('' + value, 4)
}
export function areUUIDsValid (values: string[]) {
return isArray(values) && values.every(v => isUUIDValid(v))
}
export function isIdOrUUIDValid (value: string) {
return isIdValid(value) || isUUIDValid(value)
}
export function isBooleanValid (value: any) {
return typeof value === 'boolean' || (typeof value === 'string' && validator.default.isBoolean(value))
}
export function isIntOrNull (value: any) {
return value === null || validator.default.isInt('' + value)
}
// ---------------------------------------------------------------------------
export function isFileValid (options: {
files: UploadFilesForCheck
maxSize: number | null
mimeTypeRegex: string | null
field?: string
optional?: boolean // Default false
}) {
const { files, mimeTypeRegex, field, maxSize, optional = false } = options
// Should have files
if (!files) return optional
const fileArray = isArray(files)
? files
: files[field]
if (!fileArray || !isArray(fileArray) || fileArray.length === 0) {
return optional
}
// The file exists
const file = fileArray[0]
if (!file?.originalname) return false
// Check size
if ((maxSize !== null) && file.size > maxSize) return false
if (mimeTypeRegex === null) return true
return checkMimetypeRegex(file.mimetype, mimeTypeRegex)
}
export function checkMimetypeRegex (fileMimeType: string, mimeTypeRegex: string) {
return new RegExp(`^${mimeTypeRegex}$`, 'i').test(fileMimeType)
}
// ---------------------------------------------------------------------------
export function toCompleteUUID (value: string) {
if (isShortUUID(value)) {
try {
return shortToUUID(value)
} catch {
return ''
}
}
return value
}
export function toCompleteUUIDs (values: string[]) {
return values.map(v => toCompleteUUID(v))
}
export function toIntOrNull (value: string) {
const v = toValueOrNull(value)
if (v === null || v === undefined) return v
if (typeof v === 'number') return v
return validator.default.toInt('' + v)
}
export function toBooleanOrNull (value: any) {
const v = toValueOrNull(value)
if (v === null || v === undefined) return v
if (typeof v === 'boolean') return v
return validator.default.toBoolean('' + v)
}
export function toValueOrNull (value: string) {
if (value === 'null') return null
return value
}
export function toIntArray (value: any) {
if (!value) return []
if (isArray(value) === false) return [ validator.default.toInt(value) ]
return value.map(v => validator.default.toInt(v))
}
// ---------------------------------------------------------------------------
export function isStableVersionValid (value: string) {
if (!exists(value)) return false
const parts = (value + '').split('.')
return parts.length === 3 && parts.every(p => validator.default.isInt(p))
}
export function isStableOrUnstableVersionValid (value: string) {
if (!exists(value)) return false
// suffix is beta.x or alpha.x
const [ stable, suffix ] = value.split('-')
if (!isStableVersionValid(stable)) return false
const suffixRegex = /^(rc|alpha|beta)\.\d+$/
if (suffix && !suffixRegex.test(suffix)) return false
return true
}

View File

@@ -0,0 +1,16 @@
import { PlayerTheme, PlayerThemeVideoSetting } from '@peertube/peertube-models'
import { DEFAULT_CHANNEL_PLAYER_SETTING_VALUE, DEFAULT_INSTANCE_PLAYER_SETTING_VALUE } from '@server/initializers/constants.js'
export function isPlayerVideoThemeSettingValid (name: PlayerThemeVideoSetting) {
return isPlayerChannelThemeSettingValid(name) || name === DEFAULT_CHANNEL_PLAYER_SETTING_VALUE
}
export function isPlayerChannelThemeSettingValid (name: PlayerThemeVideoSetting) {
return name === DEFAULT_INSTANCE_PLAYER_SETTING_VALUE || isPlayerThemeValid(name as PlayerTheme)
}
const availableThemes = new Set<PlayerTheme>([ 'galaxy', 'lucide' ])
export function isPlayerThemeValid (name: PlayerTheme) {
return availableThemes.has(name)
}

View File

@@ -0,0 +1,144 @@
import { PluginPackageJSON, PluginType, PluginType_Type } from '@peertube/peertube-models'
import validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
import { isUrlValid } from './activitypub/misc.js'
import { exists, isArray, isSafePath } from './misc.js'
const PLUGINS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.PLUGINS
export function isPluginTypeValid (value: any) {
return exists(value) &&
(value === PluginType.PLUGIN || value === PluginType.THEME)
}
export function isPluginNameValid (value: string) {
return exists(value) &&
validator.default.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.NAME) &&
validator.default.matches(value, /^[a-z-0-9]+$/)
}
export function isNpmPluginNameValid (value: string) {
return exists(value) &&
validator.default.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.NAME) &&
validator.default.matches(value, /^[a-z\-._0-9]+$/) &&
(value.startsWith('peertube-plugin-') || value.startsWith('peertube-theme-'))
}
export function isPluginDescriptionValid (value: string) {
return exists(value) && validator.default.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.DESCRIPTION)
}
export function isPluginEngineValid (engine: any) {
return exists(engine) && exists(engine.peertube)
}
export function isPluginHomepage (value: string) {
return exists(value) && (!value || isUrlValid(value))
}
export function isPluginBugs (value: string) {
return exists(value) && (!value || isUrlValid(value))
}
export function areStaticDirectoriesValid (staticDirs: any) {
if (!exists(staticDirs) || typeof staticDirs !== 'object') return false
for (const key of Object.keys(staticDirs)) {
if (!isSafePath(staticDirs[key])) return false
}
return true
}
export function areClientScriptsValid (clientScripts: any[]) {
return isArray(clientScripts) &&
clientScripts.every(c => {
return isSafePath(c.script) && isArray(c.scopes)
})
}
export function areTranslationPathsValid (translations: any) {
if (!exists(translations) || typeof translations !== 'object') return false
for (const key of Object.keys(translations)) {
if (!isSafePath(translations[key])) return false
}
return true
}
export function areCSSPathsValid (css: any[]) {
return isArray(css) && css.every(c => isSafePath(c))
}
export function isThemeNameValid (name: string) {
return name && typeof name === 'string' &&
(isPluginNameValid(name) || name.startsWith('peertube-core-'))
}
export function isPackageJSONValid (packageJSON: PluginPackageJSON, pluginType: PluginType_Type) {
let result = true
const badFields: string[] = []
if (!isNpmPluginNameValid(packageJSON.name)) {
result = false
badFields.push('name')
}
if (!isPluginDescriptionValid(packageJSON.description)) {
result = false
badFields.push('description')
}
if (!isPluginEngineValid(packageJSON.engine)) {
result = false
badFields.push('engine')
}
if (!isPluginHomepage(packageJSON.homepage)) {
result = false
badFields.push('homepage')
}
if (!exists(packageJSON.author)) {
result = false
badFields.push('author')
}
if (!isPluginBugs(packageJSON.bugs)) {
result = false
badFields.push('bugs')
}
if (pluginType === PluginType.PLUGIN && !isSafePath(packageJSON.library)) {
result = false
badFields.push('library')
}
if (!areStaticDirectoriesValid(packageJSON.staticDirs)) {
result = false
badFields.push('staticDirs')
}
if (!areCSSPathsValid(packageJSON.css)) {
result = false
badFields.push('css')
}
if (!areClientScriptsValid(packageJSON.clientScripts)) {
result = false
badFields.push('clientScripts')
}
if (!areTranslationPathsValid(packageJSON.translations)) {
result = false
badFields.push('translations')
}
return { result, badFields }
}
export function isLibraryCodeValid (library: any) {
return typeof library.register === 'function' &&
typeof library.unregister === 'function'
}

View File

@@ -0,0 +1,233 @@
import {
LiveRTMPHLSTranscodingSuccess,
RunnerJobSuccessPayload,
RunnerJobType,
RunnerJobUpdatePayload,
TranscriptionSuccess,
VODAudioMergeTranscodingSuccess,
VODHLSTranscodingSuccess,
VODWebVideoTranscodingSuccess,
VideoStudioTranscodingSuccess,
GenerateStoryboardSuccess
} from '@peertube/peertube-models'
import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants.js'
import { UploadFilesForCheck } from 'express'
import validator from 'validator'
import { exists, isArray, isFileValid, isSafeFilename } from '../misc.js'
const RUNNER_JOBS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.RUNNER_JOBS
const runnerJobTypes = new Set([
'vod-hls-transcoding',
'vod-web-video-transcoding',
'vod-audio-merge-transcoding',
'live-rtmp-hls-transcoding',
'video-studio-transcoding',
'video-transcription',
'generate-video-storyboard'
])
export function isRunnerJobTypeValid (value: RunnerJobType) {
return runnerJobTypes.has(value)
}
export function isRunnerJobSuccessPayloadValid (value: RunnerJobSuccessPayload, type: RunnerJobType, files: UploadFilesForCheck) {
return isRunnerJobVODWebVideoResultPayloadValid(value as VODWebVideoTranscodingSuccess, type, files) ||
isRunnerJobVODHLSResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
isRunnerJobVODAudioMergeResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
isRunnerJobLiveRTMPHLSResultPayloadValid(value as LiveRTMPHLSTranscodingSuccess, type) ||
isRunnerJobVideoStudioResultPayloadValid(value as VideoStudioTranscodingSuccess, type, files) ||
isRunnerJobTranscriptionResultPayloadValid(value as TranscriptionSuccess, type, files) ||
isRunnerJobGenerateStoryboardResultPayloadValid(value as GenerateStoryboardSuccess, type, files)
}
// ---------------------------------------------------------------------------
export function isRunnerJobProgressValid (value: string) {
return validator.default.isInt(value + '', RUNNER_JOBS_CONSTRAINTS_FIELDS.PROGRESS)
}
export function isRunnerJobUpdatePayloadValid (value: RunnerJobUpdatePayload, type: RunnerJobType, files: UploadFilesForCheck) {
return isRunnerJobVODWebVideoUpdatePayloadValid(value, type, files) ||
isRunnerJobVODHLSUpdatePayloadValid(value, type, files) ||
isRunnerJobVideoStudioUpdatePayloadValid(value, type, files) ||
isRunnerJobVODAudioMergeUpdatePayloadValid(value, type, files) ||
isRunnerJobLiveRTMPHLSUpdatePayloadValid(value, type, files) ||
isRunnerJobTranscriptionUpdatePayloadValid(value, type, files) ||
isRunnerJobGenerateStoryboardUpdatePayloadValid(value, type, files)
}
// ---------------------------------------------------------------------------
export function isRunnerJobTokenValid (value: string) {
return exists(value) && validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.TOKEN)
}
export function isRunnerJobAbortReasonValid (value: string) {
return validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.REASON)
}
export function isRunnerJobErrorMessageValid (value: string) {
return validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.ERROR_MESSAGE)
}
export function isRunnerJobStateValid (value: any) {
return exists(value) && RUNNER_JOB_STATES[value] !== undefined
}
export function isRunnerJobArrayOfStateValid (value: any) {
return isArray(value) && value.every(v => isRunnerJobStateValid(v))
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
function isRunnerJobVODWebVideoResultPayloadValid (
_value: VODWebVideoTranscodingSuccess,
type: RunnerJobType,
files: UploadFilesForCheck
) {
return type === 'vod-web-video-transcoding' &&
isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null })
}
function isRunnerJobVODHLSResultPayloadValid (
_value: VODHLSTranscodingSuccess,
type: RunnerJobType,
files: UploadFilesForCheck
) {
return type === 'vod-hls-transcoding' &&
isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null }) &&
isFileValid({ files, field: 'payload[resolutionPlaylistFile]', mimeTypeRegex: null, maxSize: null })
}
function isRunnerJobVODAudioMergeResultPayloadValid (
_value: VODAudioMergeTranscodingSuccess,
type: RunnerJobType,
files: UploadFilesForCheck
) {
return type === 'vod-audio-merge-transcoding' &&
isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null })
}
function isRunnerJobLiveRTMPHLSResultPayloadValid (
value: LiveRTMPHLSTranscodingSuccess,
type: RunnerJobType
) {
return type === 'live-rtmp-hls-transcoding' && isEmptyUpdatePayload(value)
}
function isRunnerJobVideoStudioResultPayloadValid (
_value: VideoStudioTranscodingSuccess,
type: RunnerJobType,
files: UploadFilesForCheck
) {
return type === 'video-studio-transcoding' &&
isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null })
}
function isRunnerJobTranscriptionResultPayloadValid (
value: TranscriptionSuccess,
type: RunnerJobType,
files: UploadFilesForCheck
) {
return type === 'video-transcription' &&
isFileValid({ files, field: 'payload[vttFile]', mimeTypeRegex: null, maxSize: null })
}
function isRunnerJobGenerateStoryboardResultPayloadValid (
value: GenerateStoryboardSuccess,
type: RunnerJobType,
files: UploadFilesForCheck
) {
return type === 'generate-video-storyboard' &&
isFileValid({ files, field: 'payload[storyboardFile]', mimeTypeRegex: null, maxSize: null })
}
// ---------------------------------------------------------------------------
function isRunnerJobVODWebVideoUpdatePayloadValid (
value: RunnerJobUpdatePayload,
type: RunnerJobType,
_files: UploadFilesForCheck
) {
return type === 'vod-web-video-transcoding' && isEmptyUpdatePayload(value)
}
function isRunnerJobVODHLSUpdatePayloadValid (
value: RunnerJobUpdatePayload,
type: RunnerJobType,
_files: UploadFilesForCheck
) {
return type === 'vod-hls-transcoding' && isEmptyUpdatePayload(value)
}
function isRunnerJobVODAudioMergeUpdatePayloadValid (
value: RunnerJobUpdatePayload,
type: RunnerJobType,
_files: UploadFilesForCheck
) {
return type === 'vod-audio-merge-transcoding' && isEmptyUpdatePayload(value)
}
function isRunnerJobTranscriptionUpdatePayloadValid (
value: RunnerJobUpdatePayload,
type: RunnerJobType,
_files: UploadFilesForCheck
) {
return type === 'video-transcription' && isEmptyUpdatePayload(value)
}
function isRunnerJobLiveRTMPHLSUpdatePayloadValid (
value: RunnerJobUpdatePayload,
type: RunnerJobType,
files: UploadFilesForCheck
) {
let result = type === 'live-rtmp-hls-transcoding' && !!value && !!files
result &&= isFileValid({ files, field: 'payload[masterPlaylistFile]', mimeTypeRegex: null, maxSize: null, optional: true })
result &&= isFileValid({
files,
field: 'payload[resolutionPlaylistFile]',
mimeTypeRegex: null,
maxSize: null,
optional: !value.resolutionPlaylistFilename
})
if (files['payload[resolutionPlaylistFile]']) {
result &&= isSafeFilename(value.resolutionPlaylistFilename, 'm3u8')
}
return result &&
isSafeFilename(value.videoChunkFilename, 'ts') &&
(
(
value.type === 'remove-chunk'
) ||
(
value.type === 'add-chunk' &&
isFileValid({ files, field: 'payload[videoChunkFile]', mimeTypeRegex: null, maxSize: null })
)
)
}
function isRunnerJobVideoStudioUpdatePayloadValid (
value: RunnerJobUpdatePayload,
type: RunnerJobType,
_files: UploadFilesForCheck
) {
return type === 'video-studio-transcoding' && isEmptyUpdatePayload(value)
}
function isRunnerJobGenerateStoryboardUpdatePayloadValid (
value: RunnerJobUpdatePayload,
type: RunnerJobType,
_files: UploadFilesForCheck
) {
return type === 'generate-video-storyboard' && isEmptyUpdatePayload(value)
}
function isEmptyUpdatePayload (value: any): boolean {
return !value || (typeof value === 'object' && Object.keys(value).length === 0)
}

View File

@@ -0,0 +1,30 @@
import validator from 'validator'
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
import { exists } from '../misc.js'
const RUNNERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.RUNNERS
function isRunnerRegistrationTokenValid (value: string) {
return exists(value) && validator.default.isLength(value, RUNNERS_CONSTRAINTS_FIELDS.TOKEN)
}
function isRunnerTokenValid (value: string) {
return exists(value) && validator.default.isLength(value, RUNNERS_CONSTRAINTS_FIELDS.TOKEN)
}
function isRunnerNameValid (value: string) {
return exists(value) && validator.default.isLength(value, RUNNERS_CONSTRAINTS_FIELDS.NAME)
}
function isRunnerDescriptionValid (value: string) {
return exists(value) && validator.default.isLength(value, RUNNERS_CONSTRAINTS_FIELDS.DESCRIPTION)
}
// ---------------------------------------------------------------------------
export {
isRunnerRegistrationTokenValid,
isRunnerTokenValid,
isRunnerNameValid,
isRunnerDescriptionValid
}

View File

@@ -0,0 +1,37 @@
import validator from 'validator'
import { SearchTargetType } from '@peertube/peertube-models'
import { CONFIG } from '@server/initializers/config.js'
import { exists, isArray } from './misc.js'
function isNumberArray (value: any) {
return isArray(value) && value.every(v => validator.default.isInt('' + v))
}
function isStringArray (value: any) {
return isArray(value) && value.every(v => typeof v === 'string')
}
function isBooleanBothQueryValid (value: any) {
return value === 'true' || value === 'false' || value === 'both'
}
function isSearchTargetValid (value: SearchTargetType) {
if (!exists(value)) return true
const searchIndexConfig = CONFIG.SEARCH.SEARCH_INDEX
if (value === 'local') return true
if (value === 'search-index' && searchIndexConfig.ENABLED) return true
return false
}
// ---------------------------------------------------------------------------
export {
isNumberArray,
isStringArray,
isBooleanBothQueryValid,
isSearchTargetValid
}

View File

@@ -0,0 +1,42 @@
import validator from 'validator'
import { CONFIG } from '@server/initializers/config.js'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
import { exists, isArray } from './misc.js'
function isHostValid (host: string) {
const isURLOptions = {
require_host: true,
require_tld: true
}
// We validate 'localhost', so we don't have the top level domain
if (CONFIG.WEBSERVER.HOSTNAME === 'localhost' || CONFIG.WEBSERVER.HOSTNAME === '127.0.0.1') {
isURLOptions.require_tld = false
}
return exists(host) && validator.default.isURL(host, isURLOptions) && host.split('://').length === 1
}
function isEachUniqueHostValid (hosts: string[]) {
return isArray(hosts) &&
hosts.every(host => {
return isHostValid(host) && hosts.indexOf(host) === hosts.lastIndexOf(host)
})
}
function isValidContactBody (value: any) {
return exists(value) && validator.default.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.BODY)
}
function isValidContactFromName (value: any) {
return exists(value) && validator.default.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.FROM_NAME)
}
// ---------------------------------------------------------------------------
export {
isValidContactBody,
isValidContactFromName,
isEachUniqueHostValid,
isHostValid
}

View File

@@ -0,0 +1,23 @@
import validator from 'validator'
import { UserNotificationSettingValue } from '@peertube/peertube-models'
import { exists } from './misc.js'
function isUserNotificationTypeValid (value: any) {
return exists(value) && validator.default.isInt('' + value)
}
function isUserNotificationSettingValid (value: any) {
return exists(value) &&
validator.default.isInt('' + value) &&
(
value === UserNotificationSettingValue.NONE ||
value === UserNotificationSettingValue.WEB ||
value === UserNotificationSettingValue.EMAIL ||
value === (UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL)
)
}
export {
isUserNotificationSettingValid,
isUserNotificationTypeValid
}

View File

@@ -0,0 +1,25 @@
import validator from 'validator'
import { CONSTRAINTS_FIELDS, USER_REGISTRATION_STATES } from '../../initializers/constants.js'
import { exists } from './misc.js'
const USER_REGISTRATIONS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USER_REGISTRATIONS
function isRegistrationStateValid (value: string) {
return exists(value) && USER_REGISTRATION_STATES[value] !== undefined
}
function isRegistrationModerationResponseValid (value: string) {
return exists(value) && validator.default.isLength(value, USER_REGISTRATIONS_CONSTRAINTS_FIELDS.MODERATOR_MESSAGE)
}
function isRegistrationReasonValid (value: string) {
return exists(value) && validator.default.isLength(value, USER_REGISTRATIONS_CONSTRAINTS_FIELDS.REASON_MESSAGE)
}
// ---------------------------------------------------------------------------
export {
isRegistrationStateValid,
isRegistrationModerationResponseValid,
isRegistrationReasonValid
}

View File

@@ -0,0 +1,114 @@
import { AVAILABLE_LOCALES } from '@peertube/peertube-core-utils'
import { UserRole } from '@peertube/peertube-models'
import validator from 'validator'
import { isEmailEnabled } from '../../initializers/config.js'
import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants.js'
import { exists, isArray, isBooleanValid } from './misc.js'
const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
export function isUserPasswordValid (value: string) {
return validator.default.isLength(value, { min: USERS_CONSTRAINTS_FIELDS.PASSWORD.min }) &&
isUserPasswordTooLong(value) !== true
}
export function isUserPasswordTooLong (value: string) {
return !validator.default.isLength(value, { max: USERS_CONSTRAINTS_FIELDS.PASSWORD.max }) ||
Buffer.byteLength(value, 'utf8') > USERS_CONSTRAINTS_FIELDS.PASSWORD.maxBytes
}
export function isUserPasswordValidOrEmpty (value: string) {
// Empty password is only possible if emailing is enabled.
if (value === '') return isEmailEnabled()
return isUserPasswordValid(value)
}
export function isUserVideoQuotaValid (value: string) {
return exists(value) && validator.default.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA)
}
export function isUserVideoQuotaDailyValid (value: string) {
return exists(value) && validator.default.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA_DAILY)
}
export function isUserUsernameValid (value: string) {
return exists(value) &&
validator.default.matches(value, new RegExp(`^[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?$`)) &&
validator.default.isLength(value, USERS_CONSTRAINTS_FIELDS.USERNAME)
}
export function isUserDisplayNameValid (value: string) {
return value === null || (exists(value) && validator.default.isLength(value, CONSTRAINTS_FIELDS.USERS.NAME))
}
export function isUserDescriptionValid (value: string) {
return value === null || (exists(value) && validator.default.isLength(value, CONSTRAINTS_FIELDS.USERS.DESCRIPTION))
}
export function isUserEmailVerifiedValid (value: any) {
return isBooleanValid(value)
}
const nsfwPolicies = new Set(Object.values(NSFW_POLICY_TYPES))
export function isUserNSFWPolicyValid (value: any) {
return exists(value) && nsfwPolicies.has(value)
}
export function isUserP2PEnabledValid (value: any) {
return isBooleanValid(value)
}
export function isUserVideosHistoryEnabledValid (value: any) {
return isBooleanValid(value)
}
export function isUserAutoPlayVideoValid (value: any) {
return isBooleanValid(value)
}
export function isUserVideoLanguages (value: any) {
return value === null || (isArray(value) && value.length < CONSTRAINTS_FIELDS.USERS.VIDEO_LANGUAGES.max)
}
export function isUserLanguage (value: any) {
return value === null || AVAILABLE_LOCALES.includes(value)
}
export function isUserAdminFlagsValid (value: any) {
return exists(value) && validator.default.isInt('' + value)
}
export function isUserBlockedValid (value: any) {
return isBooleanValid(value)
}
export function isUserAutoPlayNextVideoValid (value: any) {
return isBooleanValid(value)
}
export function isUserAutoPlayNextVideoPlaylistValid (value: any) {
return isBooleanValid(value)
}
export function isUserEmailPublicValid (value: any) {
return isBooleanValid(value)
}
export function isUserNoModal (value: any) {
return isBooleanValid(value)
}
export function isUserBlockedReasonValid (value: any) {
return value === null || (exists(value) && validator.default.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON))
}
export function isUserRoleValid (value: any) {
return exists(value) &&
validator.default.isInt('' + value) &&
[ UserRole.ADMINISTRATOR, UserRole.MODERATOR, UserRole.USER ].includes(value)
}
export function isUserFeatureInfo (value: string) {
return exists(value) && validator.default.isInt(value + '')
}

View File

@@ -0,0 +1,22 @@
import validator from 'validator'
import { VideoBlacklistType } from '@peertube/peertube-models'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
import { exists } from './misc.js'
const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST
function isVideoBlacklistReasonValid (value: string) {
return value === null || validator.default.isLength(value, VIDEO_BLACKLIST_CONSTRAINTS_FIELDS.REASON)
}
function isVideoBlacklistTypeValid (value: any) {
return exists(value) &&
(value === VideoBlacklistType.AUTO_BEFORE_PUBLISHED || value === VideoBlacklistType.MANUAL)
}
// ---------------------------------------------------------------------------
export {
isVideoBlacklistReasonValid,
isVideoBlacklistTypeValid
}

View File

@@ -0,0 +1,43 @@
import { UploadFilesForCheck } from 'express'
import { readFile } from 'fs/promises'
import { getFileSize } from '@peertube/peertube-node-utils'
import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers/constants.js'
import { logger } from '../logger.js'
import { exists, isFileValid } from './misc.js'
function isVideoCaptionLanguageValid (value: any) {
return exists(value) && VIDEO_LANGUAGES[value] !== undefined
}
// MacOS sends application/octet-stream
const videoCaptionTypesRegex = [ ...Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT), 'application/octet-stream' ]
.map(m => `(${m})`)
.join('|')
function isVideoCaptionFile (files: UploadFilesForCheck, field: string) {
return isFileValid({
files,
mimeTypeRegex: videoCaptionTypesRegex,
field,
maxSize: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
})
}
async function isVTTFileValid (filePath: string) {
const size = await getFileSize(filePath)
const content = await readFile(filePath, 'utf8')
logger.debug('Checking VTT file %s', filePath, { size, content })
if (size > CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max) return false
return content?.startsWith('WEBVTT')
}
// ---------------------------------------------------------------------------
export {
isVideoCaptionFile,
isVTTFileValid,
isVideoCaptionLanguageValid
}

View File

@@ -0,0 +1,6 @@
import { VIDEO_CHANNEL_SYNC_STATE } from '@server/initializers/constants.js'
import { exists } from './misc.js'
export function isVideoChannelSyncStateValid (value: any) {
return exists(value) && VIDEO_CHANNEL_SYNC_STATE[value] !== undefined
}

View File

@@ -0,0 +1,32 @@
import validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
import { exists } from './misc.js'
import { isUserUsernameValid } from './users.js'
const VIDEO_CHANNELS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_CHANNELS
function isVideoChannelUsernameValid (value: string) {
// Use the same constraints than user username
return isUserUsernameValid(value)
}
function isVideoChannelDescriptionValid (value: string) {
return value === null || validator.default.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.DESCRIPTION)
}
function isVideoChannelDisplayNameValid (value: string) {
return exists(value) && validator.default.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.NAME)
}
function isVideoChannelSupportValid (value: string) {
return value === null || (exists(value) && validator.default.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.SUPPORT))
}
// ---------------------------------------------------------------------------
export {
isVideoChannelUsernameValid,
isVideoChannelDescriptionValid,
isVideoChannelDisplayNameValid,
isVideoChannelSupportValid
}

View File

@@ -0,0 +1,26 @@
import { isArray } from './misc.js'
import { VideoChapter, VideoChapterUpdate } from '@peertube/peertube-models'
import { Unpacked } from '@peertube/peertube-typescript-utils'
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
import validator from 'validator'
export function areVideoChaptersValid (value: VideoChapter[]) {
if (!isArray(value)) return false
if (!value.every(v => isVideoChapterValid(v))) return false
const timecodes = value.map(c => c.timecode)
return new Set(timecodes).size === timecodes.length
}
export function isVideoChapterValid (value: Unpacked<VideoChapterUpdate['chapters']>) {
return isVideoChapterTimecodeValid(value.timecode) && isVideoChapterTitleValid(value.title)
}
export function isVideoChapterTitleValid (value: any) {
return validator.default.isLength(value + '', CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE)
}
export function isVideoChapterTimecodeValid (value: any) {
return validator.default.isInt(value + '', { min: 0 })
}

View File

@@ -0,0 +1,14 @@
import validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS
function isValidVideoCommentText (value: string) {
return value === null || validator.default.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT)
}
// ---------------------------------------------------------------------------
export {
isValidVideoCommentText
}

View File

@@ -0,0 +1,46 @@
import 'multer'
import { UploadFilesForCheck } from 'express'
import validator from 'validator'
import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers/constants.js'
import { exists, isFileValid } from './misc.js'
function isVideoImportTargetUrlValid (url: string) {
const isURLOptions = {
require_host: true,
require_tld: true,
require_protocol: true,
require_valid_protocol: true,
protocols: [ 'http', 'https' ]
}
return exists(url) &&
validator.default.isURL('' + url, isURLOptions) &&
validator.default.isLength('' + url, CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL)
}
function isVideoImportStateValid (value: any) {
return exists(value) && VIDEO_IMPORT_STATES[value] !== undefined
}
// MacOS sends application/octet-stream
const videoTorrentImportRegex = [ ...Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT), 'application/octet-stream' ]
.map(m => `(${m})`)
.join('|')
function isVideoImportTorrentFile (files: UploadFilesForCheck) {
return isFileValid({
files,
mimeTypeRegex: videoTorrentImportRegex,
field: 'torrentfile',
maxSize: CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max,
optional: true
})
}
// ---------------------------------------------------------------------------
export {
isVideoImportStateValid,
isVideoImportTargetUrlValid,
isVideoImportTorrentFile
}

View File

@@ -0,0 +1,18 @@
import { LiveVideoLatencyMode } from '@peertube/peertube-models'
import { isDateValid } from './misc.js'
export function isLiveLatencyModeValid (value: any) {
return [ LiveVideoLatencyMode.DEFAULT, LiveVideoLatencyMode.SMALL_LATENCY, LiveVideoLatencyMode.HIGH_LATENCY ].includes(value)
}
export function isLiveScheduleValid (schedule: any) {
return isDateValid(schedule?.startAt)
}
export function areLiveSchedulesValid (schedules: any[]) {
if (!schedules) return true
if (!Array.isArray(schedules)) return false
return schedules.every(schedule => isLiveScheduleValid(schedule))
}

View File

@@ -0,0 +1,35 @@
import { exists } from './misc.js'
import validator from 'validator'
import { CONSTRAINTS_FIELDS, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PLAYLIST_TYPES } from '../../initializers/constants.js'
const PLAYLISTS_CONSTRAINT_FIELDS = CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS
function isVideoPlaylistNameValid (value: any) {
return exists(value) && validator.default.isLength(value, PLAYLISTS_CONSTRAINT_FIELDS.NAME)
}
function isVideoPlaylistDescriptionValid (value: any) {
return value === null || (exists(value) && validator.default.isLength(value, PLAYLISTS_CONSTRAINT_FIELDS.DESCRIPTION))
}
function isVideoPlaylistPrivacyValid (value: number) {
return validator.default.isInt(value + '') && VIDEO_PLAYLIST_PRIVACIES[value] !== undefined
}
function isVideoPlaylistTimestampValid (value: any) {
return value === null || (exists(value) && validator.default.isInt('' + value, { min: 0 }))
}
function isVideoPlaylistTypeValid (value: any) {
return exists(value) && VIDEO_PLAYLIST_TYPES[value] !== undefined
}
// ---------------------------------------------------------------------------
export {
isVideoPlaylistNameValid,
isVideoPlaylistDescriptionValid,
isVideoPlaylistPrivacyValid,
isVideoPlaylistTimestampValid,
isVideoPlaylistTypeValid
}

View File

@@ -0,0 +1,5 @@
function isRatingValid (value: any) {
return value === 'like' || value === 'dislike'
}
export { isRatingValid }

View File

@@ -0,0 +1,12 @@
import { exists } from './misc.js'
function isVideoRedundancyTarget (value: any) {
return exists(value) &&
(value === 'my-videos' || value === 'remote-videos')
}
// ---------------------------------------------------------------------------
export {
isVideoRedundancyTarget
}

View File

@@ -0,0 +1,16 @@
import { VideoStatsTimeserieMetric } from '@peertube/peertube-models'
const validMetrics = new Set<VideoStatsTimeserieMetric>([
'viewers',
'aggregateWatchTime'
])
function isValidStatTimeserieMetric (value: VideoStatsTimeserieMetric) {
return validMetrics.has(value)
}
// ---------------------------------------------------------------------------
export {
isValidStatTimeserieMetric
}

View File

@@ -0,0 +1,53 @@
import validator from 'validator'
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
import { buildTaskFileFieldname } from '@server/lib/video-studio.js'
import { VideoStudioTask } from '@peertube/peertube-models'
import { isArray } from './misc.js'
import { isVideoFileMimeTypeValid, isVideoImageValid } from './videos.js'
import { forceNumber } from '@peertube/peertube-core-utils'
function isValidStudioTasksArray (tasks: any) {
if (!isArray(tasks)) return false
return tasks.length >= CONSTRAINTS_FIELDS.VIDEO_STUDIO.TASKS.min &&
tasks.length <= CONSTRAINTS_FIELDS.VIDEO_STUDIO.TASKS.max
}
function isStudioCutTaskValid (task: VideoStudioTask) {
if (task.name !== 'cut') return false
if (!task.options) return false
const { start, end } = task.options
if (!start && !end) return false
if (start && !validator.default.isInt(start + '', CONSTRAINTS_FIELDS.VIDEO_STUDIO.CUT_TIME)) return false
if (end && !validator.default.isInt(end + '', CONSTRAINTS_FIELDS.VIDEO_STUDIO.CUT_TIME)) return false
if (!start || !end) return true
return forceNumber(start) < forceNumber(end)
}
function isStudioTaskAddIntroOutroValid (task: VideoStudioTask, indice: number, files: Express.Multer.File[]) {
const file = files.find(f => f.fieldname === buildTaskFileFieldname(indice, 'file'))
return (task.name === 'add-intro' || task.name === 'add-outro') &&
file && isVideoFileMimeTypeValid([ file ], null)
}
function isStudioTaskAddWatermarkValid (task: VideoStudioTask, indice: number, files: Express.Multer.File[]) {
const file = files.find(f => f.fieldname === buildTaskFileFieldname(indice, 'file'))
return task.name === 'add-watermark' &&
file && isVideoImageValid([ file ], null, true)
}
// ---------------------------------------------------------------------------
export {
isValidStudioTasksArray,
isStudioCutTaskValid,
isStudioTaskAddIntroOutroValid,
isStudioTaskAddWatermarkValid
}

View File

@@ -0,0 +1,5 @@
import { exists } from './misc.js'
export function isValidCreateTranscodingType (value: any) {
return exists(value) && (value === 'hls' || value === 'web-video')
}

View File

@@ -0,0 +1,27 @@
import { VideoStatsUserAgentDevice } from '@peertube/peertube-models'
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
import validator from 'validator'
import { exists } from './misc.js'
export function isVideoTimeValid (value: number, videoDuration?: number) {
if (value < 0) return false
if (exists(videoDuration) && value > videoDuration) return false
return true
}
export function isVideoViewEvent (value: string) {
return value === 'seek'
}
export function isVideoViewUAInfo (value: string) {
return validator.default.isLength(value, CONSTRAINTS_FIELDS.VIDEO_VIEW.UA_INFO)
}
// See https://docs.uaparser.dev/info/device/type.html
const devices = new Set<VideoStatsUserAgentDevice>([ 'console', 'embedded', 'mobile', 'smarttv', 'tablet', 'wearable', 'xr', 'desktop' ])
export function toVideoViewUADeviceOrNull (value: VideoStatsUserAgentDevice) {
return devices.has(value)
? value
: null
}

View File

@@ -0,0 +1,205 @@
import { HttpStatusCode, VideoIncludeType, VideoPrivacy, VideoPrivacyType, VideoRateType } from '@peertube/peertube-models'
import { getVideoWithAttributes } from '@server/helpers/video.js'
import { Request, Response, UploadFilesForCheck } from 'express'
import { decode as magnetUriDecode } from 'magnet-uri'
import validator from 'validator'
import {
CONSTRAINTS_FIELDS,
MIMETYPES,
VIDEO_CATEGORIES,
VIDEO_COMMENTS_POLICY,
VIDEO_LICENCES,
VIDEO_LIVE,
VIDEO_PRIVACIES,
VIDEO_RATE_TYPES,
VIDEO_STATES
} from '../../initializers/constants.js'
import { exists, isArray, isDateValid, isFileValid } from './misc.js'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
export function isVideoIncludeValid (include: VideoIncludeType) {
return exists(include) && validator.default.isInt('' + include)
}
export function isVideoCategoryValid (value: any) {
return value === null || VIDEO_CATEGORIES[value] !== undefined
}
export function isVideoStateValid (value: any) {
return exists(value) && VIDEO_STATES[value] !== undefined
}
export function isVideoLicenceValid (value: any) {
return value === null || VIDEO_LICENCES[value] !== undefined
}
export function isVideoLanguageValid (value: any) {
return value === null ||
(typeof value === 'string' && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.LANGUAGE))
}
export function isVideoDurationValid (value: string) {
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION)
}
export function isVideoDescriptionValid (value: string) {
return value === null || (exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION))
}
export function isVideoCommentsPolicyValid (value: any) {
return value === null || VIDEO_COMMENTS_POLICY[value] !== undefined
}
export function isVideoSupportValid (value: string) {
return value === null || (exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.SUPPORT))
}
export function isVideoNameValid (value: string) {
return exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
}
export function isVideoSourceFilenameValid (value: string) {
return exists(value) && validator.default.isLength(value, CONSTRAINTS_FIELDS.VIDEO_SOURCE.FILENAME)
}
export function isVideoTagValid (tag: string) {
return exists(tag) && validator.default.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG)
}
export function areVideoTagsValid (tags: string[]) {
return tags === null || (
isArray(tags) &&
validator.default.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) &&
tags.every(tag => isVideoTagValid(tag))
)
}
export function isVideoViewsValid (value: string | number) {
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS)
}
const ratingTypes = new Set(Object.values(VIDEO_RATE_TYPES))
export function isVideoRatingTypeValid (value: string) {
return value === 'none' || ratingTypes.has(value as VideoRateType)
}
export function isVideoFileExtnameValid (value: string) {
return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined)
}
export function isVideoFileMimeTypeValid (files: UploadFilesForCheck, field = 'videofile') {
return isFileValid({
files,
mimeTypeRegex: MIMETYPES.VIDEO.MIMETYPES_REGEX,
field,
maxSize: null
})
}
const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
.map(v => v.replace('.', ''))
.join('|')
const videoImageTypesRegex = `image/(${videoImageTypes})`
export function isVideoImageValid (files: UploadFilesForCheck, field: string, optional = true) {
return isFileValid({
files,
mimeTypeRegex: videoImageTypesRegex,
field,
maxSize: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max,
optional
})
}
export function isVideoPrivacyValid (value: number) {
return VIDEO_PRIVACIES[value] !== undefined
}
export function isVideoReplayPrivacyValid (value: number) {
return VIDEO_PRIVACIES[value] !== undefined && value !== VideoPrivacy.PASSWORD_PROTECTED
}
export function isScheduleVideoUpdatePrivacyValid (value: number) {
return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC || value === VideoPrivacy.INTERNAL
}
export function isVideoOriginallyPublishedAtValid (value: string | null) {
return value === null || isDateValid(value)
}
export function isVideoFileInfoHashValid (value: string | null | undefined) {
return exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
}
export function isVideoFileResolutionValid (value: string) {
return exists(value) && validator.default.isInt(value + '')
}
export function isVideoFPSResolutionValid (value: string) {
return value === null || validator.default.isInt(value + '')
}
export function isVideoFileSizeValid (value: string) {
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE)
}
export function isVideoMagnetUriValid (value: string) {
if (!exists(value)) return false
const parsed = magnetUriDecode(value)
return parsed && isVideoFileInfoHashValid(parsed.infoHash)
}
export function isPasswordValid (password: string) {
return password.length >= CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.min &&
password.length < CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.max
}
export function isValidPasswordProtectedPrivacy (req: Request, res: Response) {
const fail = (message: string) => {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message
})
return false
}
let privacy: VideoPrivacyType
const video = getVideoWithAttributes(res)
if (exists(req.body?.privacy)) privacy = req.body.privacy
else if (exists(video?.privacy)) privacy = video.privacy
if (privacy !== VideoPrivacy.PASSWORD_PROTECTED) return true
if (!exists(req.body.videoPasswords) && !exists(req.body.passwords) && !exists(req.body.password)) {
return fail('Video passwords are missing.')
}
const passwords = req.body.videoPasswords || req.body.passwords || [ req.body.password ]
if (passwords.length === 0) return fail('At least one video password is required.')
if (new Set(passwords).size !== passwords.length) return fail('Duplicate video passwords are not allowed.')
for (const password of passwords) {
if (typeof password !== 'string') {
return fail('Video password should be a string.')
}
if (!isPasswordValid(password)) {
return fail('Invalid video password. Password length should be at least 2 characters and no more than 100 characters.')
}
}
return true
}
export function isNSFWFlagsValid (value: number) {
return value === null || (exists(value) && validator.default.isInt('' + value))
}
export function isNSFWSummaryValid (value: any) {
return value === null || (exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NSFW_SUMMARY))
}

View File

@@ -0,0 +1,17 @@
import validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
import { exists, isArray } from './misc.js'
export function isWatchedWordListNameValid (listName: string) {
return exists(listName) && validator.default.isLength(listName, CONSTRAINTS_FIELDS.WATCHED_WORDS.LIST_NAME)
}
export function isWatchedWordValid (word: string) {
return exists(word) && validator.default.isLength(word, CONSTRAINTS_FIELDS.WATCHED_WORDS.WORD)
}
export function areWatchedWordsValid (words: string[]) {
return isArray(words) &&
validator.default.isInt(words.length.toString(), CONSTRAINTS_FIELDS.WATCHED_WORDS.WORDS) &&
words.every(word => isWatchedWordValid(word))
}

View File

@@ -0,0 +1,15 @@
import { REMOTE_SCHEME, WEBSERVER } from '../../initializers/constants.js'
import { sanitizeHost } from '../core-utils.js'
import { exists } from './misc.js'
export function isWebfingerLocalResourceValid (value: string) {
if (!exists(value)) return false
if (value.startsWith('acct:') === false) return false
const actorWithHost = value.substring(5)
const actorParts = actorWithHost.split('@')
if (actorParts.length !== 2) return false
const host = actorParts[1]
return sanitizeHost(host, REMOTE_SCHEME.HTTP) === WEBSERVER.HOST
}