Init commit
This commit is contained in:
68
server/core/helpers/custom-validators/abuses.ts
Normal file
68
server/core/helpers/custom-validators/abuses.ts
Normal 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
|
||||
}
|
||||
22
server/core/helpers/custom-validators/accounts.ts
Normal file
22
server/core/helpers/custom-validators/accounts.ts
Normal 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
|
||||
}
|
||||
150
server/core/helpers/custom-validators/activitypub/activity.ts
Normal file
150
server/core/helpers/custom-validators/activitypub/activity.ts
Normal 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)
|
||||
}
|
||||
132
server/core/helpers/custom-validators/activitypub/actor.ts
Normal file
132
server/core/helpers/custom-validators/activitypub/actor.ts
Normal 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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
75
server/core/helpers/custom-validators/activitypub/misc.ts
Normal file
75
server/core/helpers/custom-validators/activitypub/misc.ts
Normal 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')
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
279
server/core/helpers/custom-validators/activitypub/videos.ts
Normal file
279
server/core/helpers/custom-validators/activitypub/videos.ts
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
17
server/core/helpers/custom-validators/actor-images.ts
Normal file
17
server/core/helpers/custom-validators/actor-images.ts
Normal 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
|
||||
})
|
||||
}
|
||||
53
server/core/helpers/custom-validators/browse-videos.ts
Normal file
53
server/core/helpers/custom-validators/browse-videos.ts
Normal 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
|
||||
}
|
||||
5
server/core/helpers/custom-validators/bulk.ts
Normal file
5
server/core/helpers/custom-validators/bulk.ts
Normal 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'
|
||||
}
|
||||
23
server/core/helpers/custom-validators/config.ts
Normal file
23
server/core/helpers/custom-validators/config.ts
Normal 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
|
||||
})
|
||||
}
|
||||
23
server/core/helpers/custom-validators/feeds.ts
Normal file
23
server/core/helpers/custom-validators/feeds.ts
Normal 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
|
||||
}
|
||||
30
server/core/helpers/custom-validators/follows.ts
Normal file
30
server/core/helpers/custom-validators/follows.ts
Normal 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
|
||||
}
|
||||
21
server/core/helpers/custom-validators/jobs.ts
Normal file
21
server/core/helpers/custom-validators/jobs.ts
Normal 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
|
||||
}
|
||||
42
server/core/helpers/custom-validators/logs.ts
Normal file
42
server/core/helpers/custom-validators/logs.ts
Normal 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
|
||||
}
|
||||
3
server/core/helpers/custom-validators/metrics.ts
Normal file
3
server/core/helpers/custom-validators/metrics.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function isValidPlayerMode (value: any) {
|
||||
return value === 'web-video' || value === 'p2p-media-loader'
|
||||
}
|
||||
196
server/core/helpers/custom-validators/misc.ts
Normal file
196
server/core/helpers/custom-validators/misc.ts
Normal 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
|
||||
}
|
||||
16
server/core/helpers/custom-validators/player-settings.ts
Normal file
16
server/core/helpers/custom-validators/player-settings.ts
Normal 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)
|
||||
}
|
||||
144
server/core/helpers/custom-validators/plugins.ts
Normal file
144
server/core/helpers/custom-validators/plugins.ts
Normal 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'
|
||||
}
|
||||
233
server/core/helpers/custom-validators/runners/jobs.ts
Normal file
233
server/core/helpers/custom-validators/runners/jobs.ts
Normal 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)
|
||||
}
|
||||
30
server/core/helpers/custom-validators/runners/runners.ts
Normal file
30
server/core/helpers/custom-validators/runners/runners.ts
Normal 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
|
||||
}
|
||||
37
server/core/helpers/custom-validators/search.ts
Normal file
37
server/core/helpers/custom-validators/search.ts
Normal 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
|
||||
}
|
||||
42
server/core/helpers/custom-validators/servers.ts
Normal file
42
server/core/helpers/custom-validators/servers.ts
Normal 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
|
||||
}
|
||||
23
server/core/helpers/custom-validators/user-notifications.ts
Normal file
23
server/core/helpers/custom-validators/user-notifications.ts
Normal 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
|
||||
}
|
||||
25
server/core/helpers/custom-validators/user-registration.ts
Normal file
25
server/core/helpers/custom-validators/user-registration.ts
Normal 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
|
||||
}
|
||||
114
server/core/helpers/custom-validators/users.ts
Normal file
114
server/core/helpers/custom-validators/users.ts
Normal 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 + '')
|
||||
}
|
||||
22
server/core/helpers/custom-validators/video-blacklist.ts
Normal file
22
server/core/helpers/custom-validators/video-blacklist.ts
Normal 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
|
||||
}
|
||||
43
server/core/helpers/custom-validators/video-captions.ts
Normal file
43
server/core/helpers/custom-validators/video-captions.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
32
server/core/helpers/custom-validators/video-channels.ts
Normal file
32
server/core/helpers/custom-validators/video-channels.ts
Normal 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
|
||||
}
|
||||
26
server/core/helpers/custom-validators/video-chapters.ts
Normal file
26
server/core/helpers/custom-validators/video-chapters.ts
Normal 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 })
|
||||
}
|
||||
14
server/core/helpers/custom-validators/video-comments.ts
Normal file
14
server/core/helpers/custom-validators/video-comments.ts
Normal 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
|
||||
}
|
||||
46
server/core/helpers/custom-validators/video-imports.ts
Normal file
46
server/core/helpers/custom-validators/video-imports.ts
Normal 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
|
||||
}
|
||||
18
server/core/helpers/custom-validators/video-lives.ts
Normal file
18
server/core/helpers/custom-validators/video-lives.ts
Normal 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))
|
||||
}
|
||||
35
server/core/helpers/custom-validators/video-playlists.ts
Normal file
35
server/core/helpers/custom-validators/video-playlists.ts
Normal 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
|
||||
}
|
||||
5
server/core/helpers/custom-validators/video-rates.ts
Normal file
5
server/core/helpers/custom-validators/video-rates.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
function isRatingValid (value: any) {
|
||||
return value === 'like' || value === 'dislike'
|
||||
}
|
||||
|
||||
export { isRatingValid }
|
||||
12
server/core/helpers/custom-validators/video-redundancies.ts
Normal file
12
server/core/helpers/custom-validators/video-redundancies.ts
Normal 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
|
||||
}
|
||||
16
server/core/helpers/custom-validators/video-stats.ts
Normal file
16
server/core/helpers/custom-validators/video-stats.ts
Normal 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
|
||||
}
|
||||
53
server/core/helpers/custom-validators/video-studio.ts
Normal file
53
server/core/helpers/custom-validators/video-studio.ts
Normal 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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { exists } from './misc.js'
|
||||
|
||||
export function isValidCreateTranscodingType (value: any) {
|
||||
return exists(value) && (value === 'hls' || value === 'web-video')
|
||||
}
|
||||
27
server/core/helpers/custom-validators/video-view.ts
Normal file
27
server/core/helpers/custom-validators/video-view.ts
Normal 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
|
||||
}
|
||||
205
server/core/helpers/custom-validators/videos.ts
Normal file
205
server/core/helpers/custom-validators/videos.ts
Normal 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))
|
||||
}
|
||||
17
server/core/helpers/custom-validators/watched-words.ts
Normal file
17
server/core/helpers/custom-validators/watched-words.ts
Normal 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))
|
||||
}
|
||||
15
server/core/helpers/custom-validators/webfinger.ts
Normal file
15
server/core/helpers/custom-validators/webfinger.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user