mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-04 10:19:35 +02:00
server/server -> server/core
This commit is contained in:
parent
114327d4ce
commit
5a3d0650c9
838 changed files with 111 additions and 111 deletions
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
|
||||
}
|
151
server/core/helpers/custom-validators/activitypub/activity.ts
Normal file
151
server/core/helpers/custom-validators/activitypub/activity.ts
Normal file
|
@ -0,0 +1,151 @@
|
|||
import validator from 'validator'
|
||||
import { Activity, ActivityType } from '@peertube/peertube-models'
|
||||
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 { isPlaylistObjectValid } from './playlist.js'
|
||||
import { sanitizeAndCheckVideoCommentObject } from './video-comments.js'
|
||||
import { sanitizeAndCheckVideoTorrentObject } from './videos.js'
|
||||
import { isWatchActionObjectValid } from './watch-action.js'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function isActivityValid (activity: any) {
|
||||
const checker = activityCheckers[activity.type]
|
||||
// Unknown activity type
|
||||
if (!checker) return false
|
||||
|
||||
return checker(activity)
|
||||
}
|
||||
|
||||
function isFlagActivityValid (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Flag') &&
|
||||
isAbuseReasonValid(activity.content) &&
|
||||
isActivityPubUrlValid(activity.object)
|
||||
}
|
||||
|
||||
function isLikeActivityValid (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Like') &&
|
||||
isObjectValid(activity.object)
|
||||
}
|
||||
|
||||
function isDislikeActivityValid (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Dislike') &&
|
||||
isObjectValid(activity.object)
|
||||
}
|
||||
|
||||
function isAnnounceActivityValid (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Announce') &&
|
||||
isObjectValid(activity.object)
|
||||
}
|
||||
|
||||
function isViewActivityValid (activity: any) {
|
||||
return isBaseActivityValid(activity, 'View') &&
|
||||
isActivityPubUrlValid(activity.actor) &&
|
||||
isActivityPubUrlValid(activity.object)
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
function isUpdateActivityValid (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Update') &&
|
||||
(
|
||||
isCacheFileObjectValid(activity.object) ||
|
||||
isPlaylistObjectValid(activity.object) ||
|
||||
sanitizeAndCheckVideoTorrentObject(activity.object) ||
|
||||
sanitizeAndCheckActorObject(activity.object)
|
||||
)
|
||||
}
|
||||
|
||||
function isDeleteActivityValid (activity: any) {
|
||||
// We don't really check objects
|
||||
return isBaseActivityValid(activity, 'Delete') &&
|
||||
isObjectValid(activity.object)
|
||||
}
|
||||
|
||||
function isFollowActivityValid (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Follow') &&
|
||||
isObjectValid(activity.object)
|
||||
}
|
||||
|
||||
function isAcceptActivityValid (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Accept')
|
||||
}
|
||||
|
||||
function isRejectActivityValid (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Reject')
|
||||
}
|
||||
|
||||
function isUndoActivityValid (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Undo') &&
|
||||
(
|
||||
isFollowActivityValid(activity.object) ||
|
||||
isLikeActivityValid(activity.object) ||
|
||||
isDislikeActivityValid(activity.object) ||
|
||||
isAnnounceActivityValid(activity.object) ||
|
||||
isCreateActivityValid(activity.object)
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isRootActivityValid,
|
||||
isActivityValid,
|
||||
isFlagActivityValid,
|
||||
isLikeActivityValid,
|
||||
isDislikeActivityValid,
|
||||
isAnnounceActivityValid,
|
||||
isViewActivityValid,
|
||||
isCreateActivityValid,
|
||||
isUpdateActivityValid,
|
||||
isDeleteActivityValid,
|
||||
isFollowActivityValid,
|
||||
isAcceptActivityValid,
|
||||
isRejectActivityValid,
|
||||
isUndoActivityValid
|
||||
}
|
142
server/core/helpers/custom-validators/activitypub/actor.ts
Normal file
142
server/core/helpers/custom-validators/activitypub/actor.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
import validator from 'validator'
|
||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
||||
import { exists, isArray, isDateValid } from '../misc.js'
|
||||
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc.js'
|
||||
import { isHostValid } from '../servers.js'
|
||||
import { peertubeTruncate } from '@server/helpers/core-utils.js'
|
||||
|
||||
function isActorEndpointsObjectValid (endpointObject: any) {
|
||||
if (endpointObject?.sharedInbox) {
|
||||
return isActivityPubUrlValid(endpointObject.sharedInbox)
|
||||
}
|
||||
|
||||
// Shared inbox is optional
|
||||
return true
|
||||
}
|
||||
|
||||
function isActorPublicKeyObjectValid (publicKeyObject: any) {
|
||||
return isActivityPubUrlValid(publicKeyObject.id) &&
|
||||
isActivityPubUrlValid(publicKeyObject.owner) &&
|
||||
isActorPublicKeyValid(publicKeyObject.publicKeyPem)
|
||||
}
|
||||
|
||||
function isActorTypeValid (type: string) {
|
||||
return type === 'Person' || type === 'Application' || type === 'Group' || type === 'Service' || type === 'Organization'
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.:]'
|
||||
const actorNameRegExp = new RegExp(`^${actorNameAlphabet}+$`)
|
||||
function isActorPreferredUsernameValid (preferredUsername: string) {
|
||||
return exists(preferredUsername) && validator.default.matches(preferredUsername, actorNameRegExp)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function isActorFollowingCountValid (value: string) {
|
||||
return exists(value) && validator.default.isInt('' + value, { min: 0 })
|
||||
}
|
||||
|
||||
function isActorFollowersCountValid (value: string) {
|
||||
return exists(value) && validator.default.isInt('' + value, { min: 0 })
|
||||
}
|
||||
|
||||
function isActorDeleteActivityValid (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Delete')
|
||||
}
|
||||
|
||||
function sanitizeAndCheckActorObject (actor: any) {
|
||||
if (!isActorTypeValid(actor.type)) return false
|
||||
|
||||
normalizeActor(actor)
|
||||
|
||||
return exists(actor) &&
|
||||
isActivityPubUrlValid(actor.id) &&
|
||||
isActivityPubUrlValid(actor.inbox) &&
|
||||
isActorPreferredUsernameValid(actor.preferredUsername) &&
|
||||
isActivityPubUrlValid(actor.url) &&
|
||||
isActorPublicKeyObjectValid(actor.publicKey) &&
|
||||
isActorEndpointsObjectValid(actor.endpoints) &&
|
||||
|
||||
(!actor.outbox || isActivityPubUrlValid(actor.outbox)) &&
|
||||
(!actor.following || isActivityPubUrlValid(actor.following)) &&
|
||||
(!actor.followers || isActivityPubUrlValid(actor.followers)) &&
|
||||
|
||||
setValidAttributedTo(actor) &&
|
||||
setValidDescription(actor) &&
|
||||
// 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)
|
||||
}
|
||||
|
||||
function normalizeActor (actor: any) {
|
||||
if (!actor) return
|
||||
|
||||
if (!actor.url) {
|
||||
actor.url = actor.id
|
||||
} else if (typeof actor.url !== 'string') {
|
||||
actor.url = actor.url.href || actor.url.url
|
||||
}
|
||||
|
||||
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 isValidActorHandle (handle: string) {
|
||||
if (!exists(handle)) return false
|
||||
|
||||
const parts = handle.split('@')
|
||||
if (parts.length !== 2) return false
|
||||
|
||||
return isHostValid(parts[1])
|
||||
}
|
||||
|
||||
function areValidActorHandles (handles: string[]) {
|
||||
return isArray(handles) && handles.every(h => isValidActorHandle(h))
|
||||
}
|
||||
|
||||
function setValidDescription (obj: any) {
|
||||
if (!obj.summary) obj.summary = null
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
normalizeActor,
|
||||
actorNameAlphabet,
|
||||
areValidActorHandles,
|
||||
isActorEndpointsObjectValid,
|
||||
isActorPublicKeyObjectValid,
|
||||
isActorTypeValid,
|
||||
isActorPublicKeyValid,
|
||||
isActorPreferredUsernameValid,
|
||||
isActorPrivateKeyValid,
|
||||
isActorFollowingCountValid,
|
||||
isActorFollowersCountValid,
|
||||
isActorDeleteActivityValid,
|
||||
sanitizeAndCheckActorObject,
|
||||
isValidActorHandle
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { CacheFileObject } from '@peertube/peertube-models'
|
||||
import { exists, isDateValid } from '../misc.js'
|
||||
import { isActivityPubUrlValid } from './misc.js'
|
||||
import { isRemoteVideoUrlValid } from './videos.js'
|
||||
|
||||
function isCacheFileObjectValid (object: CacheFileObject) {
|
||||
return exists(object) &&
|
||||
object.type === 'CacheFile' &&
|
||||
(object.expires === null || isDateValid(object.expires)) &&
|
||||
isActivityPubUrlValid(object.object) &&
|
||||
(isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isCacheFileObjectValid
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isPlaylistRedundancyUrlValid (url: any) {
|
||||
return url.type === 'Link' &&
|
||||
(url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
|
||||
isActivityPubUrlValid(url.href)
|
||||
}
|
76
server/core/helpers/custom-validators/activitypub/misc.ts
Normal file
76
server/core/helpers/custom-validators/activitypub/misc.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import validator from 'validator'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
||||
import { exists } from '../misc.js'
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function isActivityPubUrlValid (url: string) {
|
||||
return isUrlValid(url) && validator.default.isLength('' + url, CONSTRAINTS_FIELDS.ACTORS.URL)
|
||||
}
|
||||
|
||||
function isBaseActivityValid (activity: any, type: string) {
|
||||
return activity.type === type &&
|
||||
isActivityPubUrlValid(activity.id) &&
|
||||
isObjectValid(activity.actor) &&
|
||||
isUrlCollectionValid(activity.to) &&
|
||||
isUrlCollectionValid(activity.cc)
|
||||
}
|
||||
|
||||
function isUrlCollectionValid (collection: any) {
|
||||
return collection === undefined ||
|
||||
(Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t)))
|
||||
}
|
||||
|
||||
function isObjectValid (object: any) {
|
||||
return exists(object) &&
|
||||
(
|
||||
isActivityPubUrlValid(object) || isActivityPubUrlValid(object.id)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
export {
|
||||
isUrlValid,
|
||||
isActivityPubUrlValid,
|
||||
isBaseActivityValid,
|
||||
setValidAttributedTo,
|
||||
isObjectValid,
|
||||
isActivityPubVideoDurationValid
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import validator from 'validator'
|
||||
import { PlaylistElementObject, PlaylistObject } from '@peertube/peertube-models'
|
||||
import { exists, isDateValid, isUUIDValid } from '../misc.js'
|
||||
import { isVideoPlaylistNameValid } from '../video-playlists.js'
|
||||
import { isActivityPubUrlValid } from './misc.js'
|
||||
|
||||
function isPlaylistObjectValid (object: PlaylistObject) {
|
||||
return exists(object) &&
|
||||
object.type === 'Playlist' &&
|
||||
validator.default.isInt(object.totalItems + '') &&
|
||||
isVideoPlaylistNameValid(object.name) &&
|
||||
isUUIDValid(object.uuid) &&
|
||||
isDateValid(object.published) &&
|
||||
isDateValid(object.updated)
|
||||
}
|
||||
|
||||
function isPlaylistElementObjectValid (object: PlaylistElementObject) {
|
||||
return exists(object) &&
|
||||
object.type === 'PlaylistElement' &&
|
||||
validator.default.isInt(object.position + '') &&
|
||||
isActivityPubUrlValid(object.url)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isPlaylistObjectValid,
|
||||
isPlaylistElementObjectValid
|
||||
}
|
|
@ -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,59 @@
|
|||
import validator from 'validator'
|
||||
import { ACTIVITY_PUB } from '../../../initializers/constants.js'
|
||||
import { exists, isArray, isDateValid } from '../misc.js'
|
||||
import { isActivityPubUrlValid } from './misc.js'
|
||||
|
||||
function sanitizeAndCheckVideoCommentObject (comment: any) {
|
||||
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) &&
|
||||
isArray(comment.to) &&
|
||||
(
|
||||
comment.to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ||
|
||||
comment.cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1
|
||||
) // Only accept public 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
|
||||
}
|
247
server/core/helpers/custom-validators/activitypub/videos.ts
Normal file
247
server/core/helpers/custom-validators/activitypub/videos.ts
Normal file
|
@ -0,0 +1,247 @@
|
|||
import validator from 'validator'
|
||||
import {
|
||||
ActivityPubStoryboard,
|
||||
ActivityTrackerUrlObject,
|
||||
ActivityVideoFileMetadataUrlObject,
|
||||
LiveVideoLatencyMode,
|
||||
VideoObject,
|
||||
VideoState
|
||||
} from '@peertube/peertube-models'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
|
||||
import { peertubeTruncate } from '../../core-utils.js'
|
||||
import { isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc.js'
|
||||
import { isLiveLatencyModeValid } from '../video-lives.js'
|
||||
import {
|
||||
isVideoDescriptionValid,
|
||||
isVideoDurationValid,
|
||||
isVideoNameValid,
|
||||
isVideoStateValid,
|
||||
isVideoTagValid,
|
||||
isVideoViewsValid
|
||||
} from '../videos.js'
|
||||
import { isActivityPubUrlValid, isActivityPubVideoDurationValid, isBaseActivityValid, setValidAttributedTo } from './misc.js'
|
||||
|
||||
function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Update') &&
|
||||
sanitizeAndCheckVideoTorrentObject(activity.object)
|
||||
}
|
||||
|
||||
function sanitizeAndCheckVideoTorrentObject (video: any) {
|
||||
if (!video || video.type !== 'Video') return false
|
||||
|
||||
if (!setValidRemoteTags(video)) {
|
||||
logger.debug('Video has invalid tags', { video })
|
||||
return false
|
||||
}
|
||||
if (!setValidRemoteVideoUrls(video)) {
|
||||
logger.debug('Video has invalid urls', { video })
|
||||
return false
|
||||
}
|
||||
if (!setRemoteVideoContent(video)) {
|
||||
logger.debug('Video has invalid content', { video })
|
||||
return false
|
||||
}
|
||||
if (!setValidAttributedTo(video)) {
|
||||
logger.debug('Video has invalid attributedTo', { video })
|
||||
return false
|
||||
}
|
||||
if (!setValidRemoteCaptions(video)) {
|
||||
logger.debug('Video has invalid captions', { video })
|
||||
return false
|
||||
}
|
||||
if (!setValidRemoteIcon(video)) {
|
||||
logger.debug('Video has invalid icons', { video })
|
||||
return false
|
||||
}
|
||||
if (!setValidStoryboard(video)) {
|
||||
logger.debug('Video has invalid preview (storyboard)', { video })
|
||||
return false
|
||||
}
|
||||
|
||||
// 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.commentsEnabled)) video.commentsEnabled = false
|
||||
if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
|
||||
if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
|
||||
if (!isBooleanValid(video.permanentLive)) video.permanentLive = false
|
||||
if (!isLiveLatencyModeValid(video.latencyMode)) video.latencyMode = LiveVideoLatencyMode.DEFAULT
|
||||
|
||||
return isActivityPubUrlValid(video.id) &&
|
||||
isVideoNameValid(video.name) &&
|
||||
isActivityPubVideoDurationValid(video.duration) &&
|
||||
isVideoDurationValid(video.duration.replace(/[^0-9]+/g, '')) &&
|
||||
isUUIDValid(video.uuid) &&
|
||||
(!video.category || isRemoteNumberIdentifierValid(video.category)) &&
|
||||
(!video.licence || isRemoteNumberIdentifierValid(video.licence)) &&
|
||||
(!video.language || isRemoteStringIdentifierValid(video.language)) &&
|
||||
isVideoViewsValid(video.views) &&
|
||||
isBooleanValid(video.sensitive) &&
|
||||
isDateValid(video.published) &&
|
||||
isDateValid(video.updated) &&
|
||||
(!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
|
||||
(!video.uploadDate || isDateValid(video.uploadDate)) &&
|
||||
(!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
|
||||
video.attributedTo.length !== 0
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function isAPVideoFileUrlMetadataObject (url: any): url is ActivityVideoFileMetadataUrlObject {
|
||||
return url &&
|
||||
url.type === 'Link' &&
|
||||
url.mediaType === 'application/json' &&
|
||||
isArray(url.rel) && url.rel.includes('metadata')
|
||||
}
|
||||
|
||||
function isAPVideoTrackerUrlObject (url: any): url is ActivityTrackerUrlObject {
|
||||
return isArray(url.rel) &&
|
||||
url.rel.includes('tracker') &&
|
||||
isActivityPubUrlValid(url.href)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
sanitizeAndCheckVideoTorrentUpdateActivity,
|
||||
isRemoteStringIdentifierValid,
|
||||
sanitizeAndCheckVideoTorrentObject,
|
||||
isRemoteVideoUrlValid,
|
||||
isAPVideoFileUrlMetadataObject,
|
||||
isAPVideoTrackerUrlObject
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function setValidRemoteTags (video: any) {
|
||||
if (Array.isArray(video.tag) === false) return false
|
||||
|
||||
video.tag = video.tag.filter(t => {
|
||||
return t.type === 'Hashtag' &&
|
||||
isVideoTagValid(t.name)
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function setValidRemoteCaptions (video: any) {
|
||||
if (!video.subtitleLanguage) video.subtitleLanguage = []
|
||||
|
||||
if (Array.isArray(video.subtitleLanguage) === false) return false
|
||||
|
||||
video.subtitleLanguage = video.subtitleLanguage.filter(caption => {
|
||||
if (!isActivityPubUrlValid(caption.url)) caption.url = null
|
||||
|
||||
return 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' && 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: any) {
|
||||
if (video.content) {
|
||||
video.content = peertubeTruncate(video.content, { length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max })
|
||||
}
|
||||
|
||||
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,37 @@
|
|||
import { WatchActionObject } from '@peertube/peertube-models'
|
||||
import { exists, isDateValid, isUUIDValid } from '../misc.js'
|
||||
import { isVideoTimeValid } from '../video-view.js'
|
||||
import { isActivityPubVideoDurationValid, isObjectValid } from './misc.js'
|
||||
|
||||
function isWatchActionObjectValid (action: WatchActionObject) {
|
||||
return exists(action) &&
|
||||
action.type === 'WatchAction' &&
|
||||
isObjectValid(action.id) &&
|
||||
isActivityPubVideoDurationValid(action.duration) &&
|
||||
isDateValid(action.startTime) &&
|
||||
isDateValid(action.endTime) &&
|
||||
isLocationValid(action.location) &&
|
||||
isUUIDValid(action.uuid) &&
|
||||
isObjectValid(action.object) &&
|
||||
isWatchSectionsValid(action.watchSections)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isWatchActionObjectValid
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isLocationValid (location: any) {
|
||||
if (!location) return true
|
||||
|
||||
return typeof location === 'object' && typeof location.addressCountry === 'string'
|
||||
}
|
||||
|
||||
function isWatchSectionsValid (sections: WatchActionObject['watchSections']) {
|
||||
return Array.isArray(sections) && sections.every(s => {
|
||||
return isVideoTimeValid(s.startTimestamp) && isVideoTimeValid(s.endTimestamp)
|
||||
})
|
||||
}
|
24
server/core/helpers/custom-validators/actor-images.ts
Normal file
24
server/core/helpers/custom-validators/actor-images.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
|
||||
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})`
|
||||
|
||||
function isActorImageFile (files: UploadFilesForCheck, fieldname: string) {
|
||||
return isFileValid({
|
||||
files,
|
||||
mimeTypeRegex: imageMimeTypesRegex,
|
||||
field: fieldname,
|
||||
maxSize: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isActorImageFile
|
||||
}
|
9
server/core/helpers/custom-validators/bulk.ts
Normal file
9
server/core/helpers/custom-validators/bulk.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
function isBulkRemoveCommentsOfScopeValid (value: string) {
|
||||
return value === 'my-videos' || value === 'instance'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isBulkRemoveCommentsOfScopeValid
|
||||
}
|
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: JobState[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed', 'paused', 'waiting-children' ]
|
||||
|
||||
function isValidJobState (value: JobState) {
|
||||
return exists(value) && jobStates.includes(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
|
||||
}
|
10
server/core/helpers/custom-validators/metrics.ts
Normal file
10
server/core/helpers/custom-validators/metrics.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
function isValidPlayerMode (value: any) {
|
||||
// TODO: remove webtorrent in v7
|
||||
return value === 'webtorrent' || value === 'web-video' || value === 'p2p-media-loader'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isValidPlayerMode
|
||||
}
|
190
server/core/helpers/custom-validators/misc.ts
Normal file
190
server/core/helpers/custom-validators/misc.ts
Normal file
|
@ -0,0 +1,190 @@
|
|||
import 'multer'
|
||||
import { UploadFilesForCheck } from 'express'
|
||||
import { sep } from 'path'
|
||||
import validator from 'validator'
|
||||
import { isShortUUID, shortToUUID } from '@peertube/peertube-node-utils'
|
||||
|
||||
function exists (value: any) {
|
||||
return value !== undefined && value !== null
|
||||
}
|
||||
|
||||
function isSafePath (p: string) {
|
||||
return exists(p) &&
|
||||
(p + '').split(sep).every(part => {
|
||||
return [ '..' ].includes(part) === false
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function isSafePeerTubeFilenameWithoutExtension (filename: string) {
|
||||
return filename.match(/^[a-z0-9-]+$/)
|
||||
}
|
||||
|
||||
function isArray (value: any): value is any[] {
|
||||
return Array.isArray(value)
|
||||
}
|
||||
|
||||
function isNotEmptyIntArray (value: any) {
|
||||
return Array.isArray(value) && value.every(v => validator.default.isInt('' + v)) && value.length !== 0
|
||||
}
|
||||
|
||||
function isNotEmptyStringArray (value: any) {
|
||||
return Array.isArray(value) && value.every(v => typeof v === 'string' && v.length !== 0) && value.length !== 0
|
||||
}
|
||||
|
||||
function isArrayOf (value: any, validator: (value: any) => boolean) {
|
||||
return isArray(value) && value.every(v => validator(v))
|
||||
}
|
||||
|
||||
function isDateValid (value: string) {
|
||||
return exists(value) && validator.default.isISO8601(value)
|
||||
}
|
||||
|
||||
function isIdValid (value: string) {
|
||||
return exists(value) && validator.default.isInt('' + value)
|
||||
}
|
||||
|
||||
function isUUIDValid (value: string) {
|
||||
return exists(value) && validator.default.isUUID('' + value, 4)
|
||||
}
|
||||
|
||||
function areUUIDsValid (values: string[]) {
|
||||
return isArray(values) && values.every(v => isUUIDValid(v))
|
||||
}
|
||||
|
||||
function isIdOrUUIDValid (value: string) {
|
||||
return isIdValid(value) || isUUIDValid(value)
|
||||
}
|
||||
|
||||
function isBooleanValid (value: any) {
|
||||
return typeof value === 'boolean' || (typeof value === 'string' && validator.default.isBoolean(value))
|
||||
}
|
||||
|
||||
function isIntOrNull (value: any) {
|
||||
return value === null || validator.default.isInt('' + value)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function checkMimetypeRegex (fileMimeType: string, mimeTypeRegex: string) {
|
||||
return new RegExp(`^${mimeTypeRegex}$`, 'i').test(fileMimeType)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function toCompleteUUID (value: string) {
|
||||
if (isShortUUID(value)) {
|
||||
try {
|
||||
return shortToUUID(value)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function toCompleteUUIDs (values: string[]) {
|
||||
return values.map(v => toCompleteUUID(v))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function toValueOrNull (value: string) {
|
||||
if (value === 'null') return null
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
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 {
|
||||
exists,
|
||||
isArrayOf,
|
||||
isNotEmptyIntArray,
|
||||
isArray,
|
||||
isIntOrNull,
|
||||
isIdValid,
|
||||
isSafePath,
|
||||
isNotEmptyStringArray,
|
||||
isUUIDValid,
|
||||
toCompleteUUIDs,
|
||||
toCompleteUUID,
|
||||
isIdOrUUIDValid,
|
||||
isDateValid,
|
||||
toValueOrNull,
|
||||
toBooleanOrNull,
|
||||
isBooleanValid,
|
||||
toIntOrNull,
|
||||
areUUIDsValid,
|
||||
toIntArray,
|
||||
isFileValid,
|
||||
isSafePeerTubeFilenameWithoutExtension,
|
||||
isSafeFilename,
|
||||
checkMimetypeRegex
|
||||
}
|
177
server/core/helpers/custom-validators/plugins.ts
Normal file
177
server/core/helpers/custom-validators/plugins.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
import validator from 'validator'
|
||||
import { PluginPackageJSON, PluginType, PluginType_Type } from '@peertube/peertube-models'
|
||||
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
|
||||
|
||||
function isPluginTypeValid (value: any) {
|
||||
return exists(value) &&
|
||||
(value === PluginType.PLUGIN || value === PluginType.THEME)
|
||||
}
|
||||
|
||||
function isPluginNameValid (value: string) {
|
||||
return exists(value) &&
|
||||
validator.default.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.NAME) &&
|
||||
validator.default.matches(value, /^[a-z-0-9]+$/)
|
||||
}
|
||||
|
||||
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-'))
|
||||
}
|
||||
|
||||
function isPluginDescriptionValid (value: string) {
|
||||
return exists(value) && validator.default.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.DESCRIPTION)
|
||||
}
|
||||
|
||||
function isPluginStableVersionValid (value: string) {
|
||||
if (!exists(value)) return false
|
||||
|
||||
const parts = (value + '').split('.')
|
||||
|
||||
return parts.length === 3 && parts.every(p => validator.default.isInt(p))
|
||||
}
|
||||
|
||||
function isPluginStableOrUnstableVersionValid (value: string) {
|
||||
if (!exists(value)) return false
|
||||
|
||||
// suffix is beta.x or alpha.x
|
||||
const [ stable, suffix ] = value.split('-')
|
||||
if (!isPluginStableVersionValid(stable)) return false
|
||||
|
||||
const suffixRegex = /^(rc|alpha|beta)\.\d+$/
|
||||
if (suffix && !suffixRegex.test(suffix)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function isPluginEngineValid (engine: any) {
|
||||
return exists(engine) && exists(engine.peertube)
|
||||
}
|
||||
|
||||
function isPluginHomepage (value: string) {
|
||||
return exists(value) && (!value || isUrlValid(value))
|
||||
}
|
||||
|
||||
function isPluginBugs (value: string) {
|
||||
return exists(value) && (!value || isUrlValid(value))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function areClientScriptsValid (clientScripts: any[]) {
|
||||
return isArray(clientScripts) &&
|
||||
clientScripts.every(c => {
|
||||
return isSafePath(c.script) && isArray(c.scopes)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function areCSSPathsValid (css: any[]) {
|
||||
return isArray(css) && css.every(c => isSafePath(c))
|
||||
}
|
||||
|
||||
function isThemeNameValid (name: string) {
|
||||
return isPluginNameValid(name)
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
function isLibraryCodeValid (library: any) {
|
||||
return typeof library.register === 'function' &&
|
||||
typeof library.unregister === 'function'
|
||||
}
|
||||
|
||||
export {
|
||||
isPluginTypeValid,
|
||||
isPackageJSONValid,
|
||||
isThemeNameValid,
|
||||
isPluginHomepage,
|
||||
isPluginStableVersionValid,
|
||||
isPluginStableOrUnstableVersionValid,
|
||||
isPluginNameValid,
|
||||
isPluginDescriptionValid,
|
||||
isLibraryCodeValid,
|
||||
isNpmPluginNameValid
|
||||
}
|
197
server/core/helpers/custom-validators/runners/jobs.ts
Normal file
197
server/core/helpers/custom-validators/runners/jobs.ts
Normal file
|
@ -0,0 +1,197 @@
|
|||
import { UploadFilesForCheck } from 'express'
|
||||
import validator from 'validator'
|
||||
import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants.js'
|
||||
import {
|
||||
LiveRTMPHLSTranscodingSuccess,
|
||||
RunnerJobSuccessPayload,
|
||||
RunnerJobType,
|
||||
RunnerJobUpdatePayload,
|
||||
VideoStudioTranscodingSuccess,
|
||||
VODAudioMergeTranscodingSuccess,
|
||||
VODHLSTranscodingSuccess,
|
||||
VODWebVideoTranscodingSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
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' ])
|
||||
function isRunnerJobTypeValid (value: RunnerJobType) {
|
||||
return runnerJobTypes.has(value)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isRunnerJobProgressValid (value: string) {
|
||||
return validator.default.isInt(value + '', RUNNER_JOBS_CONSTRAINTS_FIELDS.PROGRESS)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isRunnerJobTokenValid (value: string) {
|
||||
return exists(value) && validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.TOKEN)
|
||||
}
|
||||
|
||||
function isRunnerJobAbortReasonValid (value: string) {
|
||||
return validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.REASON)
|
||||
}
|
||||
|
||||
function isRunnerJobErrorMessageValid (value: string) {
|
||||
return validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.ERROR_MESSAGE)
|
||||
}
|
||||
|
||||
function isRunnerJobStateValid (value: any) {
|
||||
return exists(value) && RUNNER_JOB_STATES[value] !== undefined
|
||||
}
|
||||
|
||||
function isRunnerJobArrayOfStateValid (value: any) {
|
||||
return isArray(value) && value.every(v => isRunnerJobStateValid(v))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isRunnerJobTypeValid,
|
||||
isRunnerJobSuccessPayloadValid,
|
||||
isRunnerJobUpdatePayloadValid,
|
||||
isRunnerJobTokenValid,
|
||||
isRunnerJobErrorMessageValid,
|
||||
isRunnerJobProgressValid,
|
||||
isRunnerJobAbortReasonValid,
|
||||
isRunnerJobArrayOfStateValid,
|
||||
isRunnerJobStateValid
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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' && (!value || (typeof value === 'object' && Object.keys(value).length === 0))
|
||||
}
|
||||
|
||||
function isRunnerJobVideoStudioResultPayloadValid (
|
||||
_value: VideoStudioTranscodingSuccess,
|
||||
type: RunnerJobType,
|
||||
files: UploadFilesForCheck
|
||||
) {
|
||||
return type === 'video-studio-transcoding' &&
|
||||
isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isRunnerJobVODWebVideoUpdatePayloadValid (
|
||||
value: RunnerJobUpdatePayload,
|
||||
type: RunnerJobType,
|
||||
_files: UploadFilesForCheck
|
||||
) {
|
||||
return type === 'vod-web-video-transcoding' &&
|
||||
(!value || (typeof value === 'object' && Object.keys(value).length === 0))
|
||||
}
|
||||
|
||||
function isRunnerJobVODHLSUpdatePayloadValid (
|
||||
value: RunnerJobUpdatePayload,
|
||||
type: RunnerJobType,
|
||||
_files: UploadFilesForCheck
|
||||
) {
|
||||
return type === 'vod-hls-transcoding' &&
|
||||
(!value || (typeof value === 'object' && Object.keys(value).length === 0))
|
||||
}
|
||||
|
||||
function isRunnerJobVODAudioMergeUpdatePayloadValid (
|
||||
value: RunnerJobUpdatePayload,
|
||||
type: RunnerJobType,
|
||||
_files: UploadFilesForCheck
|
||||
) {
|
||||
return type === 'vod-audio-merge-transcoding' &&
|
||||
(!value || (typeof value === 'object' && Object.keys(value).length === 0))
|
||||
}
|
||||
|
||||
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' &&
|
||||
(!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
|
||||
}
|
125
server/core/helpers/custom-validators/users.ts
Normal file
125
server/core/helpers/custom-validators/users.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
import validator from 'validator'
|
||||
import { UserRole } from '@peertube/peertube-models'
|
||||
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
|
||||
|
||||
function isUserPasswordValid (value: string) {
|
||||
return validator.default.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD)
|
||||
}
|
||||
|
||||
function isUserPasswordValidOrEmpty (value: string) {
|
||||
// Empty password is only possible if emailing is enabled.
|
||||
if (value === '') return isEmailEnabled()
|
||||
|
||||
return isUserPasswordValid(value)
|
||||
}
|
||||
|
||||
function isUserVideoQuotaValid (value: string) {
|
||||
return exists(value) && validator.default.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA)
|
||||
}
|
||||
|
||||
function isUserVideoQuotaDailyValid (value: string) {
|
||||
return exists(value) && validator.default.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA_DAILY)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function isUserDisplayNameValid (value: string) {
|
||||
return value === null || (exists(value) && validator.default.isLength(value, CONSTRAINTS_FIELDS.USERS.NAME))
|
||||
}
|
||||
|
||||
function isUserDescriptionValid (value: string) {
|
||||
return value === null || (exists(value) && validator.default.isLength(value, CONSTRAINTS_FIELDS.USERS.DESCRIPTION))
|
||||
}
|
||||
|
||||
function isUserEmailVerifiedValid (value: any) {
|
||||
return isBooleanValid(value)
|
||||
}
|
||||
|
||||
const nsfwPolicies = new Set(Object.values(NSFW_POLICY_TYPES))
|
||||
function isUserNSFWPolicyValid (value: any) {
|
||||
return exists(value) && nsfwPolicies.has(value)
|
||||
}
|
||||
|
||||
function isUserP2PEnabledValid (value: any) {
|
||||
return isBooleanValid(value)
|
||||
}
|
||||
|
||||
function isUserVideosHistoryEnabledValid (value: any) {
|
||||
return isBooleanValid(value)
|
||||
}
|
||||
|
||||
function isUserAutoPlayVideoValid (value: any) {
|
||||
return isBooleanValid(value)
|
||||
}
|
||||
|
||||
function isUserVideoLanguages (value: any) {
|
||||
return value === null || (isArray(value) && value.length < CONSTRAINTS_FIELDS.USERS.VIDEO_LANGUAGES.max)
|
||||
}
|
||||
|
||||
function isUserAdminFlagsValid (value: any) {
|
||||
return exists(value) && validator.default.isInt('' + value)
|
||||
}
|
||||
|
||||
function isUserBlockedValid (value: any) {
|
||||
return isBooleanValid(value)
|
||||
}
|
||||
|
||||
function isUserAutoPlayNextVideoValid (value: any) {
|
||||
return isBooleanValid(value)
|
||||
}
|
||||
|
||||
function isUserAutoPlayNextVideoPlaylistValid (value: any) {
|
||||
return isBooleanValid(value)
|
||||
}
|
||||
|
||||
function isUserEmailPublicValid (value: any) {
|
||||
return isBooleanValid(value)
|
||||
}
|
||||
|
||||
function isUserNoModal (value: any) {
|
||||
return isBooleanValid(value)
|
||||
}
|
||||
|
||||
function isUserBlockedReasonValid (value: any) {
|
||||
return value === null || (exists(value) && validator.default.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON))
|
||||
}
|
||||
|
||||
function isUserRoleValid (value: any) {
|
||||
return exists(value) &&
|
||||
validator.default.isInt('' + value) &&
|
||||
[ UserRole.ADMINISTRATOR, UserRole.MODERATOR, UserRole.USER ].includes(value)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isUserVideosHistoryEnabledValid,
|
||||
isUserBlockedValid,
|
||||
isUserPasswordValid,
|
||||
isUserPasswordValidOrEmpty,
|
||||
isUserVideoLanguages,
|
||||
isUserBlockedReasonValid,
|
||||
isUserRoleValid,
|
||||
isUserVideoQuotaValid,
|
||||
isUserVideoQuotaDailyValid,
|
||||
isUserUsernameValid,
|
||||
isUserAdminFlagsValid,
|
||||
isUserEmailVerifiedValid,
|
||||
isUserNSFWPolicyValid,
|
||||
isUserP2PEnabledValid,
|
||||
isUserAutoPlayVideoValid,
|
||||
isUserAutoPlayNextVideoValid,
|
||||
isUserAutoPlayNextVideoPlaylistValid,
|
||||
isUserDisplayNameValid,
|
||||
isUserDescriptionValid,
|
||||
isUserEmailPublicValid,
|
||||
isUserNoModal
|
||||
}
|
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
|
||||
}
|
11
server/core/helpers/custom-validators/video-lives.ts
Normal file
11
server/core/helpers/custom-validators/video-lives.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { LiveVideoLatencyMode } from '@peertube/peertube-models'
|
||||
|
||||
function isLiveLatencyModeValid (value: any) {
|
||||
return [ LiveVideoLatencyMode.DEFAULT, LiveVideoLatencyMode.SMALL_LATENCY, LiveVideoLatencyMode.HIGH_LATENCY ].includes(value)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isLiveLatencyModeValid
|
||||
}
|
20
server/core/helpers/custom-validators/video-ownership.ts
Normal file
20
server/core/helpers/custom-validators/video-ownership.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Response } from 'express'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { MUserId } from '@server/types/models/index.js'
|
||||
import { MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership.js'
|
||||
|
||||
function checkUserCanTerminateOwnershipChange (user: MUserId, videoChangeOwnership: MVideoChangeOwnershipFull, res: Response) {
|
||||
if (videoChangeOwnership.NextOwner.userId === user.id) {
|
||||
return true
|
||||
}
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot terminate an ownership change of another user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
export {
|
||||
checkUserCanTerminateOwnershipChange
|
||||
}
|
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
|
||||
}
|
12
server/core/helpers/custom-validators/video-transcoding.ts
Normal file
12
server/core/helpers/custom-validators/video-transcoding.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { exists } from './misc.js'
|
||||
|
||||
function isValidCreateTranscodingType (value: any) {
|
||||
return exists(value) &&
|
||||
(value === 'hls' || value === 'webtorrent' || value === 'web-video') // TODO: remove webtorrent in v7
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isValidCreateTranscodingType
|
||||
}
|
12
server/core/helpers/custom-validators/video-view.ts
Normal file
12
server/core/helpers/custom-validators/video-view.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { exists } from './misc.js'
|
||||
|
||||
function isVideoTimeValid (value: number, videoDuration?: number) {
|
||||
if (value < 0) return false
|
||||
if (exists(videoDuration) && value > videoDuration) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export {
|
||||
isVideoTimeValid
|
||||
}
|
218
server/core/helpers/custom-validators/videos.ts
Normal file
218
server/core/helpers/custom-validators/videos.ts
Normal file
|
@ -0,0 +1,218 @@
|
|||
import { Request, Response, UploadFilesForCheck } from 'express'
|
||||
import { decode as magnetUriDecode } from 'magnet-uri'
|
||||
import validator from 'validator'
|
||||
import { HttpStatusCode, VideoIncludeType, VideoPrivacy, VideoPrivacyType, VideoRateType } from '@peertube/peertube-models'
|
||||
import { getVideoWithAttributes } from '@server/helpers/video.js'
|
||||
import {
|
||||
CONSTRAINTS_FIELDS,
|
||||
MIMETYPES,
|
||||
VIDEO_CATEGORIES,
|
||||
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
|
||||
|
||||
function isVideoIncludeValid (include: VideoIncludeType) {
|
||||
return exists(include) && validator.default.isInt('' + include)
|
||||
}
|
||||
|
||||
function isVideoCategoryValid (value: any) {
|
||||
return value === null || VIDEO_CATEGORIES[value] !== undefined
|
||||
}
|
||||
|
||||
function isVideoStateValid (value: any) {
|
||||
return exists(value) && VIDEO_STATES[value] !== undefined
|
||||
}
|
||||
|
||||
function isVideoLicenceValid (value: any) {
|
||||
return value === null || VIDEO_LICENCES[value] !== undefined
|
||||
}
|
||||
|
||||
function isVideoLanguageValid (value: any) {
|
||||
return value === null ||
|
||||
(typeof value === 'string' && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.LANGUAGE))
|
||||
}
|
||||
|
||||
function isVideoDurationValid (value: string) {
|
||||
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION)
|
||||
}
|
||||
|
||||
function isVideoDescriptionValid (value: string) {
|
||||
return value === null || (exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION))
|
||||
}
|
||||
|
||||
function isVideoSupportValid (value: string) {
|
||||
return value === null || (exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.SUPPORT))
|
||||
}
|
||||
|
||||
function isVideoNameValid (value: string) {
|
||||
return exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
|
||||
}
|
||||
|
||||
function isVideoTagValid (tag: string) {
|
||||
return exists(tag) && validator.default.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG)
|
||||
}
|
||||
|
||||
function areVideoTagsValid (tags: string[]) {
|
||||
return tags === null || (
|
||||
isArray(tags) &&
|
||||
validator.default.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) &&
|
||||
tags.every(tag => isVideoTagValid(tag))
|
||||
)
|
||||
}
|
||||
|
||||
function isVideoViewsValid (value: string) {
|
||||
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS)
|
||||
}
|
||||
|
||||
const ratingTypes = new Set(Object.values(VIDEO_RATE_TYPES))
|
||||
function isVideoRatingTypeValid (value: string) {
|
||||
return value === 'none' || ratingTypes.has(value as VideoRateType)
|
||||
}
|
||||
|
||||
function isVideoFileExtnameValid (value: string) {
|
||||
return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined)
|
||||
}
|
||||
|
||||
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})`
|
||||
|
||||
function isVideoImageValid (files: UploadFilesForCheck, field: string, optional = true) {
|
||||
return isFileValid({
|
||||
files,
|
||||
mimeTypeRegex: videoImageTypesRegex,
|
||||
field,
|
||||
maxSize: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max,
|
||||
optional
|
||||
})
|
||||
}
|
||||
|
||||
function isVideoPrivacyValid (value: number) {
|
||||
return VIDEO_PRIVACIES[value] !== undefined
|
||||
}
|
||||
|
||||
function isVideoReplayPrivacyValid (value: number) {
|
||||
return VIDEO_PRIVACIES[value] !== undefined && value !== VideoPrivacy.PASSWORD_PROTECTED
|
||||
}
|
||||
|
||||
function isScheduleVideoUpdatePrivacyValid (value: number) {
|
||||
return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC || value === VideoPrivacy.INTERNAL
|
||||
}
|
||||
|
||||
function isVideoOriginallyPublishedAtValid (value: string | null) {
|
||||
return value === null || isDateValid(value)
|
||||
}
|
||||
|
||||
function isVideoFileInfoHashValid (value: string | null | undefined) {
|
||||
return exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
|
||||
}
|
||||
|
||||
function isVideoFileResolutionValid (value: string) {
|
||||
return exists(value) && validator.default.isInt(value + '')
|
||||
}
|
||||
|
||||
function isVideoFPSResolutionValid (value: string) {
|
||||
return value === null || validator.default.isInt(value + '')
|
||||
}
|
||||
|
||||
function isVideoFileSizeValid (value: string) {
|
||||
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE)
|
||||
}
|
||||
|
||||
function isVideoMagnetUriValid (value: string) {
|
||||
if (!exists(value)) return false
|
||||
|
||||
const parsed = magnetUriDecode(value)
|
||||
return parsed && isVideoFileInfoHashValid(parsed.infoHash)
|
||||
}
|
||||
|
||||
function isPasswordValid (password: string) {
|
||||
return password.length >= CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.min &&
|
||||
password.length < CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.max
|
||||
}
|
||||
|
||||
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)) return fail('Video passwords are missing.')
|
||||
|
||||
const passwords = req.body.videoPasswords || req.body.passwords
|
||||
|
||||
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 {
|
||||
isVideoCategoryValid,
|
||||
isVideoLicenceValid,
|
||||
isVideoLanguageValid,
|
||||
isVideoDescriptionValid,
|
||||
isVideoFileInfoHashValid,
|
||||
isVideoNameValid,
|
||||
areVideoTagsValid,
|
||||
isVideoFPSResolutionValid,
|
||||
isScheduleVideoUpdatePrivacyValid,
|
||||
isVideoOriginallyPublishedAtValid,
|
||||
isVideoMagnetUriValid,
|
||||
isVideoStateValid,
|
||||
isVideoIncludeValid,
|
||||
isVideoViewsValid,
|
||||
isVideoRatingTypeValid,
|
||||
isVideoFileExtnameValid,
|
||||
isVideoFileMimeTypeValid,
|
||||
isVideoDurationValid,
|
||||
isVideoTagValid,
|
||||
isVideoPrivacyValid,
|
||||
isVideoReplayPrivacyValid,
|
||||
isVideoFileResolutionValid,
|
||||
isVideoFileSizeValid,
|
||||
isVideoImageValid,
|
||||
isVideoSupportValid,
|
||||
isPasswordValid,
|
||||
isValidPasswordProtectedPrivacy
|
||||
}
|
21
server/core/helpers/custom-validators/webfinger.ts
Normal file
21
server/core/helpers/custom-validators/webfinger.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { REMOTE_SCHEME, WEBSERVER } from '../../initializers/constants.js'
|
||||
import { sanitizeHost } from '../core-utils.js'
|
||||
import { exists } from './misc.js'
|
||||
|
||||
function isWebfingerLocalResourceValid (value: string) {
|
||||
if (!exists(value)) return false
|
||||
if (value.startsWith('acct:') === false) return false
|
||||
|
||||
const actorWithHost = value.substr(5)
|
||||
const actorParts = actorWithHost.split('@')
|
||||
if (actorParts.length !== 2) return false
|
||||
|
||||
const host = actorParts[1]
|
||||
return sanitizeHost(host, REMOTE_SCHEME.HTTP) === WEBSERVER.HOST
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isWebfingerLocalResourceValid
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue