はじまりの大地

このコミットが含まれているのは:
2024-07-15 09:14:04 +09:00
コミット 6632905f32
3501個のファイルの変更1439465行の追加0行の削除
+68
ファイルの表示
@@ -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
ファイルの表示
@@ -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
}
+148
ファイルの表示
@@ -0,0 +1,148 @@
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'
export function isRootActivityValid (activity: any) {
return isCollection(activity) || isActivity(activity)
}
function isCollection (activity: any) {
return (activity.type === 'Collection' || activity.type === 'OrderedCollection') &&
validator.default.isInt(activity.totalItems, { min: 0 }) &&
Array.isArray(activity.items)
}
function isActivity (activity: any) {
return isActivityPubUrlValid(activity.id) &&
exists(activity.actor) &&
(isActivityPubUrlValid(activity.actor) || isActivityPubUrlValid(activity.actor.id))
}
// ---------------------------------------------------------------------------
const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = {
Create: isCreateActivityValid,
Update: isUpdateActivityValid,
Delete: isDeleteActivityValid,
Follow: isFollowActivityValid,
Accept: isAcceptActivityValid,
Reject: isRejectActivityValid,
Announce: isAnnounceActivityValid,
Undo: isUndoActivityValid,
Like: isLikeActivityValid,
View: isViewActivityValid,
Flag: isFlagActivityValid,
Dislike: isDislikeActivityValid,
ApproveReply: isApproveReplyActivityValid,
RejectReply: isRejectReplyActivityValid
}
export function isActivityValid (activity: any) {
const checker = activityCheckers[activity.type]
// Unknown activity type
if (!checker) return false
return checker(activity)
}
export function isFlagActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Flag') &&
isAbuseReasonValid(activity.content) &&
isActivityPubUrlValid(activity.object)
}
export function isLikeActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Like') &&
isObjectValid(activity.object)
}
export function isDislikeActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Dislike') &&
isObjectValid(activity.object)
}
export function isAnnounceActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Announce') &&
isObjectValid(activity.object)
}
export function isViewActivityValid (activity: any) {
return isBaseActivityValid(activity, 'View') &&
isActivityPubUrlValid(activity.actor) &&
isActivityPubUrlValid(activity.object)
}
export function isCreateActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
(
isViewActivityValid(activity.object) ||
isDislikeActivityValid(activity.object) ||
isFlagActivityValid(activity.object) ||
isPlaylistObjectValid(activity.object) ||
isWatchActionObjectValid(activity.object) ||
isCacheFileObjectValid(activity.object) ||
sanitizeAndCheckVideoCommentObject(activity.object) ||
sanitizeAndCheckVideoTorrentObject(activity.object)
)
}
export function isUpdateActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Update') &&
(
isCacheFileObjectValid(activity.object) ||
isPlaylistObjectValid(activity.object) ||
sanitizeAndCheckVideoTorrentObject(activity.object) ||
sanitizeAndCheckActorObject(activity.object)
)
}
export function isDeleteActivityValid (activity: any) {
// We don't really check objects
return isBaseActivityValid(activity, 'Delete') &&
isObjectValid(activity.object)
}
export function isFollowActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Follow') &&
isObjectValid(activity.object)
}
export function isAcceptActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Accept')
}
export function isRejectActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Reject')
}
export function isUndoActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Undo') &&
(
isFollowActivityValid(activity.object) ||
isLikeActivityValid(activity.object) ||
isDislikeActivityValid(activity.object) ||
isAnnounceActivityValid(activity.object) ||
isCreateActivityValid(activity.object)
)
}
export function isApproveReplyActivityValid (activity: any) {
return isBaseActivityValid(activity, 'ApproveReply') &&
isActivityPubUrlValid(activity.object) &&
isActivityPubUrlValid(activity.inReplyTo)
}
export function isRejectReplyActivityValid (activity: any) {
return isBaseActivityValid(activity, 'RejectReply') &&
isActivityPubUrlValid(activity.object) &&
isActivityPubUrlValid(activity.inReplyTo)
}
+143
ファイルの表示
@@ -0,0 +1,143 @@
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)
}
const actorTypes = new Set([ 'Person', 'Application', 'Group', 'Service', 'Organization' ])
function isActorTypeValid (type: string) {
return actorTypes.has(type)
}
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
}
+33
ファイルの表示
@@ -0,0 +1,33 @@
import { CacheFileObject } from '@peertube/peertube-models'
import { MIMETYPES } from '@server/initializers/constants.js'
import validator from 'validator'
import { isDateValid } from '../misc.js'
import { isActivityPubUrlValid } from './misc.js'
export function isCacheFileObjectValid (object: CacheFileObject) {
if (!object || object.type !== 'CacheFile') return false
return (!object.expires || isDateValid(object.expires)) &&
isActivityPubUrlValid(object.object) &&
(isRedundancyUrlVideoValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
}
// ---------------------------------------------------------------------------
function isPlaylistRedundancyUrlValid (url: any) {
return url.type === 'Link' &&
(url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href)
}
// TODO: compat with < 6.1, use isRemoteVideoUrlValid instead in 7.0
function isRedundancyUrlVideoValid (url: any) {
const size = url.size || url['_:size']
const fps = url.fps || url['_fps']
return MIMETYPES.AP_VIDEO.MIMETYPE_EXT[url.mediaType] &&
isActivityPubUrlValid(url.href) &&
validator.default.isInt(url.height + '', { min: 0 }) &&
validator.default.isInt(size + '', { min: 0 }) &&
(!fps || validator.default.isInt(fps + '', { min: -1 }))
}
+76
ファイルの表示
@@ -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
}
+25
ファイルの表示
@@ -0,0 +1,25 @@
import { PlaylistElementObject, PlaylistObject } from '@peertube/peertube-models'
import validator from 'validator'
import { exists, isDateValid, isUUIDValid } from '../misc.js'
import { isVideoPlaylistNameValid } from '../video-playlists.js'
import { isActivityPubUrlValid } from './misc.js'
export function isPlaylistObjectValid (object: PlaylistObject) {
if (!object || object.type !== 'Playlist') return false
// TODO: compat with < 6.1, remove in 7.0
if (!object.uuid && object['identifier']) object.uuid = object['identifier']
return validator.default.isInt(object.totalItems + '') &&
isVideoPlaylistNameValid(object.name) &&
isUUIDValid(object.uuid) &&
isDateValid(object.published) &&
isDateValid(object.updated)
}
export function isPlaylistElementObjectValid (object: PlaylistElementObject) {
return exists(object) &&
object.type === 'PlaylistElement' &&
validator.default.isInt(object.position + '') &&
isActivityPubUrlValid(object.url)
}
+22
ファイルの表示
@@ -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
}
+15
ファイルの表示
@@ -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)
})
}
+58
ファイルの表示
@@ -0,0 +1,58 @@
import { hasAPPublic } from '@server/helpers/activity-pub-utils.js'
import validator from 'validator'
import { exists, isArray, isDateValid } from '../misc.js'
import { isActivityPubUrlValid } from './misc.js'
import { ActivityTombstoneObject, VideoCommentObject } from '@peertube/peertube-models'
function sanitizeAndCheckVideoCommentObject (comment: VideoCommentObject | ActivityTombstoneObject) {
if (!comment) return false
if (!isCommentTypeValid(comment)) return false
normalizeComment(comment)
if (comment.type === 'Tombstone') {
return isActivityPubUrlValid(comment.id) &&
isDateValid(comment.published) &&
isDateValid(comment.deleted) &&
isActivityPubUrlValid(comment.url)
}
return isActivityPubUrlValid(comment.id) &&
isCommentContentValid(comment.content) &&
isActivityPubUrlValid(comment.inReplyTo) &&
isDateValid(comment.published) &&
isActivityPubUrlValid(comment.url) &&
isArray(comment.to) &&
(!exists(comment.replyApproval) || isActivityPubUrlValid(comment.replyApproval)) &&
(hasAPPublic(comment.to) || hasAPPublic(comment.cc)) // 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
}
+255
ファイルの表示
@@ -0,0 +1,255 @@
import {
ActivityPubStoryboard,
ActivityTrackerUrlObject,
ActivityVideoFileMetadataUrlObject,
LiveVideoLatencyMode,
VideoCommentPolicy,
VideoObject,
VideoState
} from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { spdxToPeertubeLicence } from '@server/helpers/video.js'
import validator from 'validator'
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
import { peertubeTruncate } from '../../core-utils.js'
import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc.js'
import { isLiveLatencyModeValid } from '../video-lives.js'
import {
isVideoCommentsPolicyValid,
isVideoDescriptionValid,
isVideoDurationValid,
isVideoNameValid,
isVideoStateValid,
isVideoTagValid,
isVideoViewsValid
} from '../videos.js'
import { isActivityPubUrlValid, isActivityPubVideoDurationValid, isBaseActivityValid, setValidAttributedTo } from './misc.js'
export function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
return isBaseActivityValid(activity, 'Update') &&
sanitizeAndCheckVideoTorrentObject(activity.object)
}
export function sanitizeAndCheckVideoTorrentObject (video: VideoObject) {
if (!video || video.type !== 'Video') return false
const fail = (field: string) => {
logger.debug(`Video field is not valid to PeerTube: ${field}`, { video })
return false
}
if (!setValidRemoteTags(video)) return fail('tags')
if (!setValidRemoteVideoUrls(video)) return fail('urls')
if (!setRemoteVideoContent(video)) return fail('content')
if (!setValidAttributedTo(video)) return fail('attributedTo')
if (!setValidRemoteCaptions(video)) return fail('captions')
if (!setValidRemoteIcon(video)) return fail('icons')
if (!setValidStoryboard(video)) return fail('preview (storyboard)')
if (!setValidLicence(video)) return fail('licence')
// TODO: compat with < 6.1, remove in 7.0
if (!video.uuid && video['identifier']) video.uuid = video['identifier']
// Default attributes
if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true
if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
if (!isBooleanValid(video.permanentLive)) video.permanentLive = false
if (!isBooleanValid(video.sensitive)) video.sensitive = false
if (!isLiveLatencyModeValid(video.latencyMode)) video.latencyMode = LiveVideoLatencyMode.DEFAULT
if (video.commentsPolicy) {
if (!isVideoCommentsPolicyValid(video.commentsPolicy)) {
video.commentsPolicy = VideoCommentPolicy.DISABLED
}
} else if (video.commentsEnabled === true) { // Fallback to deprecated attribute
video.commentsPolicy = VideoCommentPolicy.ENABLED
} else {
video.commentsPolicy = VideoCommentPolicy.DISABLED
}
if (!isActivityPubUrlValid(video.id)) return fail('id')
if (!isVideoNameValid(video.name)) return fail('name')
if (!isActivityPubVideoDurationValid(video.duration)) return fail('duration format')
if (!isVideoDurationValid(video.duration.replace(/[^0-9]+/g, ''))) return fail('duration')
if (!isUUIDValid(video.uuid)) return fail('uuid')
if (exists(video.category) && !isRemoteNumberIdentifierValid(video.category)) return fail('category')
if (exists(video.language) && !isRemoteStringIdentifierValid(video.language)) return fail('language')
if (!isVideoViewsValid(video.views)) return fail('views')
if (!isDateValid(video.published)) return fail('published')
if (!isDateValid(video.updated)) return fail('updated')
if (exists(video.originallyPublishedAt) && !isDateValid(video.originallyPublishedAt)) return fail('originallyPublishedAt')
if (exists(video.uploadDate) && !isDateValid(video.uploadDate)) return fail('uploadDate')
if (exists(video.content) && !isRemoteVideoContentValid(video.mediaType, video.content)) return fail('mediaType/content')
if (video.attributedTo.length === 0) return fail('attributedTo')
return true
}
export function isRemoteVideoUrlValid (url: any) {
return url.type === 'Link' &&
// Video file link
(
MIMETYPES.AP_VIDEO.MIMETYPE_EXT[url.mediaType] &&
isActivityPubUrlValid(url.href) &&
validator.default.isInt(url.height + '', { min: 0 }) &&
validator.default.isInt(url.size + '', { min: 0 }) &&
(!url.fps || validator.default.isInt(url.fps + '', { min: -1 }))
) ||
// Torrent link
(
MIMETYPES.AP_TORRENT.MIMETYPE_EXT[url.mediaType] &&
isActivityPubUrlValid(url.href) &&
validator.default.isInt(url.height + '', { min: 0 })
) ||
// Magnet link
(
MIMETYPES.AP_MAGNET.MIMETYPE_EXT[url.mediaType] &&
validator.default.isLength(url.href, { min: 5 }) &&
validator.default.isInt(url.height + '', { min: 0 })
) ||
// HLS playlist link
(
(url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href) &&
isArray(url.tag)
) ||
isAPVideoTrackerUrlObject(url) ||
isAPVideoFileUrlMetadataObject(url)
}
export function isAPVideoFileUrlMetadataObject (url: any): url is ActivityVideoFileMetadataUrlObject {
return url &&
url.type === 'Link' &&
url.mediaType === 'application/json' &&
isArray(url.rel) && url.rel.includes('metadata')
}
export function isAPVideoTrackerUrlObject (url: any): url is ActivityTrackerUrlObject {
return isArray(url.rel) &&
url.rel.includes('tracker') &&
isActivityPubUrlValid(url.href)
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
function setValidRemoteTags (video: VideoObject) {
if (Array.isArray(video.tag) === false) video.tag = []
video.tag = video.tag.filter(t => t.type === 'Hashtag' && isVideoTagValid(t.name))
return true
}
function setValidRemoteCaptions (video: VideoObject) {
if (!video.subtitleLanguage) video.subtitleLanguage = []
if (Array.isArray(video.subtitleLanguage) === false) return false
video.subtitleLanguage = video.subtitleLanguage.filter(caption => {
if (!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' || mediaType === 'text/html') && isVideoDescriptionValid(content)
}
function setValidRemoteIcon (video: any) {
if (video.icon && !isArray(video.icon)) video.icon = [ video.icon ]
if (!video.icon) video.icon = []
video.icon = video.icon.filter(icon => {
return icon.type === 'Image' &&
isActivityPubUrlValid(icon.url) &&
icon.mediaType === 'image/jpeg' &&
validator.default.isInt(icon.width + '', { min: 0 }) &&
validator.default.isInt(icon.height + '', { min: 0 })
})
return video.icon.length !== 0
}
function setValidRemoteVideoUrls (video: any) {
if (Array.isArray(video.url) === false) return false
video.url = video.url.filter(u => isRemoteVideoUrlValid(u))
return true
}
function setRemoteVideoContent (video: VideoObject) {
if (video.content) {
video.content = peertubeTruncate(video.content, { length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max })
}
return true
}
function setValidLicence (video: VideoObject) {
if (!exists(video.licence)) return true
if (validator.default.isInt(video.licence.identifier)) return isRemoteNumberIdentifierValid(video.licence)
const spdx = spdxToPeertubeLicence(video.licence.identifier)
video.licence.identifier = spdx
? spdx + ''
: undefined
return true
}
function setValidStoryboard (video: VideoObject) {
if (!video.preview) return true
if (!Array.isArray(video.preview)) return false
video.preview = video.preview.filter(p => isStorybordValid(p))
return true
}
function isStorybordValid (preview: ActivityPubStoryboard) {
if (!preview) return false
if (
preview.type !== 'Image' ||
!isArray(preview.rel) ||
!preview.rel.includes('storyboard')
) {
return false
}
preview.url = preview.url.filter(u => {
return u.mediaType === 'image/jpeg' &&
isActivityPubUrlValid(u.href) &&
validator.default.isInt(u.width + '', { min: 0 }) &&
validator.default.isInt(u.height + '', { min: 0 }) &&
validator.default.isInt(u.tileWidth + '', { min: 0 }) &&
validator.default.isInt(u.tileHeight + '', { min: 0 }) &&
isActivityPubVideoDurationValid(u.tileDuration)
})
return preview.url.length !== 0
}
+51
ファイルの表示
@@ -0,0 +1,51 @@
import { arrayify } from '@peertube/peertube-core-utils'
import { WatchActionObject } from '@peertube/peertube-models'
import { isDateValid, isUUIDValid } from '../misc.js'
import { isVideoTimeValid } from '../video-view.js'
import { isActivityPubVideoDurationValid, isObjectValid } from './misc.js'
function isWatchActionObjectValid (action: WatchActionObject) {
if (!action || action.type !== 'WatchAction') return false
// TODO: compat with < 6.1, remove in 7.0
if (!action.uuid && action['identifier']) action.uuid = action['identifier']
if (action['_:actionStatus'] && !action.actionStatus) action.actionStatus = action['_:actionStatus']
if (action['_:watchSections'] && !action.watchSections) action.watchSections = arrayify(action['_:watchSections'])
return isObjectValid(action.id) &&
isActivityPubVideoDurationValid(action.duration) &&
isDateValid(action.startTime) &&
isDateValid(action.endTime) &&
isLocationValid(action.location) &&
isUUIDValid(action.uuid) &&
isObjectValid(action.object) &&
areWatchSectionsValid(action.watchSections)
}
// ---------------------------------------------------------------------------
export {
isWatchActionObjectValid
}
// ---------------------------------------------------------------------------
function isLocationValid (location: any) {
if (!location) return true
if (typeof location !== 'object') return false
if (location.addressCountry && typeof location.addressCountry !== 'string') return false
if (location.addressRegion && typeof location.addressRegion !== 'string') return false
return true
}
function areWatchSectionsValid (sections: WatchActionObject['watchSections']) {
return Array.isArray(sections) && sections.every(s => {
// TODO: compat with < 6.1, remove in 7.0
if (s['_:endTimestamp'] && !s.endTimestamp) s.endTimestamp = s['_:endTimestamp']
return isVideoTimeValid(s.startTimestamp) && isVideoTimeValid(s.endTimestamp)
})
}
+23
ファイルの表示
@@ -0,0 +1,23 @@
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
ファイルの表示
@@ -0,0 +1,9 @@
function isBulkRemoveCommentsOfScopeValid (value: string) {
return value === 'my-videos' || value === 'instance'
}
// ---------------------------------------------------------------------------
export {
isBulkRemoveCommentsOfScopeValid
}
+23
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -0,0 +1,21 @@
import { JobState } from '@peertube/peertube-models'
import { jobTypes } from '@server/lib/job-queue/job-queue.js'
import { exists } from './misc.js'
const jobStates = new Set<JobState>([ 'active', 'completed', 'failed', 'waiting', 'delayed', 'paused', 'waiting-children', 'prioritized' ])
function isValidJobState (value: JobState) {
return exists(value) && jobStates.has(value)
}
function isValidJobType (value: any) {
return exists(value) && jobTypes.includes(value)
}
// ---------------------------------------------------------------------------
export {
jobStates,
isValidJobState,
isValidJobType
}
+42
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
}
+206
ファイルの表示
@@ -0,0 +1,206 @@
import {
LiveRTMPHLSTranscodingSuccess,
RunnerJobSuccessPayload,
RunnerJobType,
RunnerJobUpdatePayload,
TranscriptionSuccess,
VODAudioMergeTranscodingSuccess,
VODHLSTranscodingSuccess,
VODWebVideoTranscodingSuccess,
VideoStudioTranscodingSuccess
} from '@peertube/peertube-models'
import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants.js'
import { UploadFilesForCheck } from 'express'
import validator from 'validator'
import { exists, isArray, isFileValid, isSafeFilename } from '../misc.js'
const RUNNER_JOBS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.RUNNER_JOBS
const runnerJobTypes = new Set([ 'vod-hls-transcoding', 'vod-web-video-transcoding', 'vod-audio-merge-transcoding' ])
export function isRunnerJobTypeValid (value: RunnerJobType) {
return runnerJobTypes.has(value)
}
export function isRunnerJobSuccessPayloadValid (value: RunnerJobSuccessPayload, type: RunnerJobType, files: UploadFilesForCheck) {
return isRunnerJobVODWebVideoResultPayloadValid(value as VODWebVideoTranscodingSuccess, type, files) ||
isRunnerJobVODHLSResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
isRunnerJobVODAudioMergeResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
isRunnerJobLiveRTMPHLSResultPayloadValid(value as LiveRTMPHLSTranscodingSuccess, type) ||
isRunnerJobVideoStudioResultPayloadValid(value as VideoStudioTranscodingSuccess, type, files) ||
isRunnerJobTranscriptionResultPayloadValid(value as TranscriptionSuccess, type, files)
}
// ---------------------------------------------------------------------------
export function isRunnerJobProgressValid (value: string) {
return validator.default.isInt(value + '', RUNNER_JOBS_CONSTRAINTS_FIELDS.PROGRESS)
}
export function isRunnerJobUpdatePayloadValid (value: RunnerJobUpdatePayload, type: RunnerJobType, files: UploadFilesForCheck) {
return isRunnerJobVODWebVideoUpdatePayloadValid(value, type, files) ||
isRunnerJobVODHLSUpdatePayloadValid(value, type, files) ||
isRunnerJobVideoStudioUpdatePayloadValid(value, type, files) ||
isRunnerJobVODAudioMergeUpdatePayloadValid(value, type, files) ||
isRunnerJobLiveRTMPHLSUpdatePayloadValid(value, type, files) ||
isRunnerJobTranscriptionUpdatePayloadValid(value, type, files)
}
// ---------------------------------------------------------------------------
export function isRunnerJobTokenValid (value: string) {
return exists(value) && validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.TOKEN)
}
export function isRunnerJobAbortReasonValid (value: string) {
return validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.REASON)
}
export function isRunnerJobErrorMessageValid (value: string) {
return validator.default.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.ERROR_MESSAGE)
}
export function isRunnerJobStateValid (value: any) {
return exists(value) && RUNNER_JOB_STATES[value] !== undefined
}
export function isRunnerJobArrayOfStateValid (value: any) {
return isArray(value) && value.every(v => isRunnerJobStateValid(v))
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
function isRunnerJobVODWebVideoResultPayloadValid (
_value: VODWebVideoTranscodingSuccess,
type: RunnerJobType,
files: UploadFilesForCheck
) {
return type === 'vod-web-video-transcoding' &&
isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null })
}
function isRunnerJobVODHLSResultPayloadValid (
_value: VODHLSTranscodingSuccess,
type: RunnerJobType,
files: UploadFilesForCheck
) {
return type === 'vod-hls-transcoding' &&
isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null }) &&
isFileValid({ files, field: 'payload[resolutionPlaylistFile]', mimeTypeRegex: null, maxSize: null })
}
function isRunnerJobVODAudioMergeResultPayloadValid (
_value: VODAudioMergeTranscodingSuccess,
type: RunnerJobType,
files: UploadFilesForCheck
) {
return type === 'vod-audio-merge-transcoding' &&
isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null })
}
function isRunnerJobLiveRTMPHLSResultPayloadValid (
value: LiveRTMPHLSTranscodingSuccess,
type: RunnerJobType
) {
return type === 'live-rtmp-hls-transcoding' && (!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 isRunnerJobTranscriptionResultPayloadValid (
value: TranscriptionSuccess,
type: RunnerJobType,
files: UploadFilesForCheck
) {
return type === 'video-transcription' &&
isFileValid({ files, field: 'payload[vttFile]', 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 isRunnerJobTranscriptionUpdatePayloadValid (
value: RunnerJobUpdatePayload,
type: RunnerJobType,
_files: UploadFilesForCheck
) {
return type === 'video-transcription' &&
(!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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
}
+6
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -0,0 +1,5 @@
function isRatingValid (value: any) {
return value === 'like' || value === 'dislike'
}
export { isRatingValid }
+12
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
}
+195
ファイルの表示
@@ -0,0 +1,195 @@
import { HttpStatusCode, VideoIncludeType, VideoPrivacy, VideoPrivacyType, VideoRateType } from '@peertube/peertube-models'
import { getVideoWithAttributes } from '@server/helpers/video.js'
import { Request, Response, UploadFilesForCheck } from 'express'
import { decode as magnetUriDecode } from 'magnet-uri'
import validator from 'validator'
import {
CONSTRAINTS_FIELDS,
MIMETYPES,
VIDEO_CATEGORIES,
VIDEO_COMMENTS_POLICY,
VIDEO_LICENCES,
VIDEO_LIVE,
VIDEO_PRIVACIES,
VIDEO_RATE_TYPES,
VIDEO_STATES
} from '../../initializers/constants.js'
import { exists, isArray, isDateValid, isFileValid } from './misc.js'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
export function isVideoIncludeValid (include: VideoIncludeType) {
return exists(include) && validator.default.isInt('' + include)
}
export function isVideoCategoryValid (value: any) {
return value === null || VIDEO_CATEGORIES[value] !== undefined
}
export function isVideoStateValid (value: any) {
return exists(value) && VIDEO_STATES[value] !== undefined
}
export function isVideoLicenceValid (value: any) {
return value === null || VIDEO_LICENCES[value] !== undefined
}
export function isVideoLanguageValid (value: any) {
return value === null ||
(typeof value === 'string' && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.LANGUAGE))
}
export function isVideoDurationValid (value: string) {
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION)
}
export function isVideoDescriptionValid (value: string) {
return value === null || (exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION))
}
export function isVideoCommentsPolicyValid (value: any) {
return value === null || VIDEO_COMMENTS_POLICY[value] !== undefined
}
export function isVideoSupportValid (value: string) {
return value === null || (exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.SUPPORT))
}
export function isVideoNameValid (value: string) {
return exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
}
export function isVideoSourceFilenameValid (value: string) {
return exists(value) && validator.default.isLength(value, CONSTRAINTS_FIELDS.VIDEO_SOURCE.FILENAME)
}
export function isVideoTagValid (tag: string) {
return exists(tag) && validator.default.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG)
}
export function areVideoTagsValid (tags: string[]) {
return tags === null || (
isArray(tags) &&
validator.default.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) &&
tags.every(tag => isVideoTagValid(tag))
)
}
export function isVideoViewsValid (value: string | number) {
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS)
}
const ratingTypes = new Set(Object.values(VIDEO_RATE_TYPES))
export function isVideoRatingTypeValid (value: string) {
return value === 'none' || ratingTypes.has(value as VideoRateType)
}
export function isVideoFileExtnameValid (value: string) {
return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined)
}
export function isVideoFileMimeTypeValid (files: UploadFilesForCheck, field = 'videofile') {
return isFileValid({
files,
mimeTypeRegex: MIMETYPES.VIDEO.MIMETYPES_REGEX,
field,
maxSize: null
})
}
const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
.map(v => v.replace('.', ''))
.join('|')
const videoImageTypesRegex = `image/(${videoImageTypes})`
export function isVideoImageValid (files: UploadFilesForCheck, field: string, optional = true) {
return isFileValid({
files,
mimeTypeRegex: videoImageTypesRegex,
field,
maxSize: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max,
optional
})
}
export function isVideoPrivacyValid (value: number) {
return VIDEO_PRIVACIES[value] !== undefined
}
export function isVideoReplayPrivacyValid (value: number) {
return VIDEO_PRIVACIES[value] !== undefined && value !== VideoPrivacy.PASSWORD_PROTECTED
}
export function isScheduleVideoUpdatePrivacyValid (value: number) {
return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC || value === VideoPrivacy.INTERNAL
}
export function isVideoOriginallyPublishedAtValid (value: string | null) {
return value === null || isDateValid(value)
}
export function isVideoFileInfoHashValid (value: string | null | undefined) {
return exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
}
export function isVideoFileResolutionValid (value: string) {
return exists(value) && validator.default.isInt(value + '')
}
export function isVideoFPSResolutionValid (value: string) {
return value === null || validator.default.isInt(value + '')
}
export function isVideoFileSizeValid (value: string) {
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE)
}
export function isVideoMagnetUriValid (value: string) {
if (!exists(value)) return false
const parsed = magnetUriDecode(value)
return parsed && isVideoFileInfoHashValid(parsed.infoHash)
}
export function isPasswordValid (password: string) {
return password.length >= CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.min &&
password.length < CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.max
}
export function isValidPasswordProtectedPrivacy (req: Request, res: Response) {
const fail = (message: string) => {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message
})
return false
}
let privacy: VideoPrivacyType
const video = getVideoWithAttributes(res)
if (exists(req.body?.privacy)) privacy = req.body.privacy
else if (exists(video?.privacy)) privacy = video.privacy
if (privacy !== VideoPrivacy.PASSWORD_PROTECTED) return true
if (!exists(req.body.videoPasswords) && !exists(req.body.passwords)) 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
}
+17
ファイルの表示
@@ -0,0 +1,17 @@
import validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
import { exists, isArray } from './misc.js'
export function isWatchedWordListNameValid (listName: string) {
return exists(listName) && validator.default.isLength(listName, CONSTRAINTS_FIELDS.WATCHED_WORDS.LIST_NAME)
}
export function isWatchedWordValid (word: string) {
return exists(word) && validator.default.isLength(word, CONSTRAINTS_FIELDS.WATCHED_WORDS.WORD)
}
export function areWatchedWordsValid (words: string[]) {
return isArray(words) &&
validator.default.isInt(words.length.toString(), CONSTRAINTS_FIELDS.WATCHED_WORDS.WORDS) &&
words.every(word => isWatchedWordValid(word))
}
+21
ファイルの表示
@@ -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
}