1
0
Fork 0

Stricter import types

Avoid forgetting to sanitize a field by specifying the sanitized object
type
This commit is contained in:
Chocobozzz 2024-02-13 09:32:17 +01:00 committed by Chocobozzz
parent 02596be702
commit 009d7b39ac
14 changed files with 147 additions and 107 deletions

View file

@ -17,48 +17,52 @@ import { exists, isArray, isDateValid, isFileValid } from './misc.js'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
function isVideoIncludeValid (include: VideoIncludeType) {
export function isVideoIncludeValid (include: VideoIncludeType) {
return exists(include) && validator.default.isInt('' + include)
}
function isVideoCategoryValid (value: any) {
export function isVideoCategoryValid (value: any) {
return value === null || VIDEO_CATEGORIES[value] !== undefined
}
function isVideoStateValid (value: any) {
export function isVideoStateValid (value: any) {
return exists(value) && VIDEO_STATES[value] !== undefined
}
function isVideoLicenceValid (value: any) {
export function isVideoLicenceValid (value: any) {
return value === null || VIDEO_LICENCES[value] !== undefined
}
function isVideoLanguageValid (value: any) {
export function isVideoLanguageValid (value: any) {
return value === null ||
(typeof value === 'string' && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.LANGUAGE))
}
function isVideoDurationValid (value: string) {
export function isVideoDurationValid (value: string) {
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION)
}
function isVideoDescriptionValid (value: string) {
export function isVideoDescriptionValid (value: string) {
return value === null || (exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION))
}
function isVideoSupportValid (value: string) {
export function isVideoSupportValid (value: string) {
return value === null || (exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.SUPPORT))
}
function isVideoNameValid (value: string) {
export function isVideoNameValid (value: string) {
return exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
}
function isVideoTagValid (tag: string) {
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)
}
function areVideoTagsValid (tags: string[]) {
export function areVideoTagsValid (tags: string[]) {
return tags === null || (
isArray(tags) &&
validator.default.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) &&
@ -66,20 +70,20 @@ function areVideoTagsValid (tags: string[]) {
)
}
function isVideoViewsValid (value: string) {
export 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) {
export function isVideoRatingTypeValid (value: string) {
return value === 'none' || ratingTypes.has(value as VideoRateType)
}
function isVideoFileExtnameValid (value: string) {
export function isVideoFileExtnameValid (value: string) {
return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined)
}
function isVideoFileMimeTypeValid (files: UploadFilesForCheck, field = 'videofile') {
export function isVideoFileMimeTypeValid (files: UploadFilesForCheck, field = 'videofile') {
return isFileValid({
files,
mimeTypeRegex: MIMETYPES.VIDEO.MIMETYPES_REGEX,
@ -93,7 +97,7 @@ const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
.join('|')
const videoImageTypesRegex = `image/(${videoImageTypes})`
function isVideoImageValid (files: UploadFilesForCheck, field: string, optional = true) {
export function isVideoImageValid (files: UploadFilesForCheck, field: string, optional = true) {
return isFileValid({
files,
mimeTypeRegex: videoImageTypesRegex,
@ -103,51 +107,51 @@ function isVideoImageValid (files: UploadFilesForCheck, field: string, optional
})
}
function isVideoPrivacyValid (value: number) {
export function isVideoPrivacyValid (value: number) {
return VIDEO_PRIVACIES[value] !== undefined
}
function isVideoReplayPrivacyValid (value: number) {
export function isVideoReplayPrivacyValid (value: number) {
return VIDEO_PRIVACIES[value] !== undefined && value !== VideoPrivacy.PASSWORD_PROTECTED
}
function isScheduleVideoUpdatePrivacyValid (value: number) {
export function isScheduleVideoUpdatePrivacyValid (value: number) {
return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC || value === VideoPrivacy.INTERNAL
}
function isVideoOriginallyPublishedAtValid (value: string | null) {
export function isVideoOriginallyPublishedAtValid (value: string | null) {
return value === null || isDateValid(value)
}
function isVideoFileInfoHashValid (value: string | null | undefined) {
export function isVideoFileInfoHashValid (value: string | null | undefined) {
return exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
}
function isVideoFileResolutionValid (value: string) {
export function isVideoFileResolutionValid (value: string) {
return exists(value) && validator.default.isInt(value + '')
}
function isVideoFPSResolutionValid (value: string) {
export function isVideoFPSResolutionValid (value: string) {
return value === null || validator.default.isInt(value + '')
}
function isVideoFileSizeValid (value: string) {
export function isVideoFileSizeValid (value: string) {
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE)
}
function isVideoMagnetUriValid (value: string) {
export function isVideoMagnetUriValid (value: string) {
if (!exists(value)) return false
const parsed = magnetUriDecode(value)
return parsed && isVideoFileInfoHashValid(parsed.infoHash)
}
function isPasswordValid (password: string) {
export 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) {
export function isValidPasswordProtectedPrivacy (req: Request, res: Response) {
const fail = (message: string) => {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
@ -184,35 +188,3 @@ function isValidPasswordProtectedPrivacy (req: Request, res: Response) {
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
}

View file

@ -414,6 +414,9 @@ const CONSTRAINTS_FIELDS = {
PARTIAL_UPLOAD_SIZE: { max: 50 * 1024 * 1024 * 1024 }, // 50GB
URL: { min: 3, max: 2000 } // Length
},
VIDEO_SOURCE: {
FILENAME: { min: 1, max: 1000 } // Length
},
VIDEO_PLAYLISTS: {
NAME: { min: 1, max: 120 }, // Length
DESCRIPTION: { min: 3, max: 1000 }, // Length

View file

@ -4,16 +4,19 @@ import { loadOrCreateVideoIfAllowedForUser } from '@server/lib/model-loaders/vid
import { userRateVideo } from '@server/lib/rate.js'
import { VideoModel } from '@server/models/video/video.js'
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
import { pick } from '@peertube/peertube-core-utils'
export abstract class AbstractRatesImporter <E, O> extends AbstractUserImporter <E, O> {
export type SanitizedRateObject = { videoUrl: string }
export abstract class AbstractRatesImporter <ROOT_OBJECT, OBJECT> extends AbstractUserImporter <ROOT_OBJECT, OBJECT, SanitizedRateObject> {
protected sanitizeRate <O extends { videoUrl: string }> (data: O) {
if (!isUrlValid(data.videoUrl)) return undefined
return data
return pick(data, [ 'videoUrl' ])
}
protected async importRate (data: { videoUrl: string }, rateType: VideoRateType) {
protected async importRate (data: SanitizedRateObject, rateType: VideoRateType) {
const videoUrl = data.videoUrl
const videoImmutable = await loadOrCreateVideoIfAllowedForUser(videoUrl)

View file

@ -7,7 +7,11 @@ import { dirname, resolve } from 'path'
const lTags = loggerTagsFactory('user-import')
export abstract class AbstractUserImporter <E, O extends { archiveFiles?: Record<string, string | Record<string, string>> }> {
export abstract class AbstractUserImporter <
ROOT_OBJECT,
OBJECT extends { archiveFiles?: Record<string, string | Record<string, string>> },
SANITIZED_OBJECT
> {
protected user: MUserDefault
protected extractedDirectory: string
protected jsonFilePath: string
@ -78,7 +82,7 @@ export abstract class AbstractUserImporter <E, O extends { archiveFiles?: Record
}
async import () {
const importData: E = await readJSON(this.jsonFilePath)
const importData: ROOT_OBJECT = await readJSON(this.jsonFilePath)
const summary = {
duplicates: 0,
success: 0,
@ -111,9 +115,9 @@ export abstract class AbstractUserImporter <E, O extends { archiveFiles?: Record
return summary
}
protected abstract getImportObjects (object: E): O[]
protected abstract getImportObjects (object: ROOT_OBJECT): OBJECT[]
protected abstract sanitize (object: O): O | undefined
protected abstract sanitize (object: OBJECT): SANITIZED_OBJECT | undefined
protected abstract importObject (object: O): Awaitable<{ duplicate: boolean }>
protected abstract importObject (object: SANITIZED_OBJECT): Awaitable<{ duplicate: boolean }>
}

View file

@ -6,12 +6,13 @@ import { ServerModel } from '@server/models/server/server.js'
import { AccountModel } from '@server/models/account/account.js'
import { isValidActorHandle } from '@server/helpers/custom-validators/activitypub/actor.js'
import { isHostValid } from '@server/helpers/custom-validators/servers.js'
import { pick } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('user-import')
type ImportObject = { handle: string | null, host: string | null, archiveFiles?: never }
export class BlocklistImporter extends AbstractUserImporter <BlocklistExportJSON, ImportObject> {
export class BlocklistImporter extends AbstractUserImporter <BlocklistExportJSON, ImportObject, ImportObject> {
protected getImportObjects (json: BlocklistExportJSON) {
return [
@ -23,7 +24,7 @@ export class BlocklistImporter extends AbstractUserImporter <BlocklistExportJSON
protected sanitize (blocklistImportData: ImportObject) {
if (!isValidActorHandle(blocklistImportData.handle) && !isHostValid(blocklistImportData.host)) return undefined
return blocklistImportData
return pick(blocklistImportData, [ 'handle', 'host' ])
}
protected async importObject (blocklistImportData: ImportObject) {

View file

@ -6,24 +6,27 @@ import { saveInTransactionWithRetries } from '@server/helpers/database-utils.js'
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
import { MAccountDefault } from '@server/types/models/index.js'
import { isUserDescriptionValid, isUserDisplayNameValid } from '@server/helpers/custom-validators/users.js'
import { pick } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('user-import')
export class AccountImporter extends AbstractUserImporter <AccountExportJSON, AccountExportJSON> {
type SanitizedObject = Pick<AccountExportJSON, 'description' | 'displayName' | 'archiveFiles'>
export class AccountImporter extends AbstractUserImporter <AccountExportJSON, AccountExportJSON, SanitizedObject> {
protected getImportObjects (json: AccountExportJSON) {
return [ json ]
}
protected sanitize (blocklistImportData: AccountExportJSON) {
if (!isUserDisplayNameValid(blocklistImportData.name)) return undefined
if (!isUserDisplayNameValid(blocklistImportData.displayName)) return undefined
if (!isUserDescriptionValid(blocklistImportData.description)) blocklistImportData.description = null
return blocklistImportData
return pick(blocklistImportData, [ 'displayName', 'description', 'archiveFiles' ])
}
protected async importObject (accountImportData: AccountExportJSON) {
protected async importObject (accountImportData: SanitizedObject) {
const account = this.user.Account
account.name = accountImportData.displayName
@ -38,7 +41,7 @@ export class AccountImporter extends AbstractUserImporter <AccountExportJSON, Ac
return { duplicate: false }
}
private async importAvatar (account: MAccountDefault, accountImportData: AccountExportJSON) {
private async importAvatar (account: MAccountDefault, accountImportData: SanitizedObject) {
const avatarPath = this.getSafeArchivePathOrThrow(accountImportData.archiveFiles.avatar)
if (!avatarPath) return undefined

View file

@ -17,23 +17,25 @@ import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
const lTags = loggerTagsFactory('user-import')
export class ChannelsImporter extends AbstractUserImporter <ChannelExportJSON, ChannelExportJSON['channels'][0]> {
type SanitizedObject = Pick<ChannelExportJSON['channels'][0], 'name' | 'displayName' | 'description' | 'support' | 'archiveFiles'>
export class ChannelsImporter extends AbstractUserImporter <ChannelExportJSON, ChannelExportJSON['channels'][0], SanitizedObject> {
protected getImportObjects (json: ChannelExportJSON) {
return json.channels
}
protected sanitize (blocklistImportData: ChannelExportJSON['channels'][0]) {
if (!isVideoChannelUsernameValid(blocklistImportData.name)) return undefined
if (!isVideoChannelDisplayNameValid(blocklistImportData.name)) return undefined
protected sanitize (channelImportData: ChannelExportJSON['channels'][0]) {
if (!isVideoChannelUsernameValid(channelImportData.name)) return undefined
if (!isVideoChannelDisplayNameValid(channelImportData.displayName)) return undefined
if (!isVideoChannelDescriptionValid(blocklistImportData.description)) blocklistImportData.description = null
if (!isVideoChannelSupportValid(blocklistImportData.support)) blocklistImportData.description = null
if (!isVideoChannelDescriptionValid(channelImportData.description)) channelImportData.description = null
if (!isVideoChannelSupportValid(channelImportData.support)) channelImportData.support = null
return blocklistImportData
return pick(channelImportData, [ 'name', 'displayName', 'description', 'support', 'archiveFiles' ])
}
protected async importObject (channelImportData: ChannelExportJSON['channels'][0]) {
protected async importObject (channelImportData: SanitizedObject) {
const account = this.user.Account
const existingChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(channelImportData.name)

View file

@ -1,5 +1,5 @@
import { DislikesExportJSON } from '@peertube/peertube-models'
import { AbstractRatesImporter } from './abstract-rates-importer.js'
import { AbstractRatesImporter, SanitizedRateObject } from './abstract-rates-importer.js'
export class DislikesImporter extends AbstractRatesImporter <DislikesExportJSON, DislikesExportJSON['dislikes'][0]> {
@ -11,7 +11,7 @@ export class DislikesImporter extends AbstractRatesImporter <DislikesExportJSON,
return this.sanitizeRate(o)
}
protected async importObject (dislikesImportData: DislikesExportJSON['dislikes'][0]) {
protected async importObject (dislikesImportData: SanitizedRateObject) {
return this.importRate(dislikesImportData, 'dislike')
}
}

View file

@ -3,10 +3,13 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { AbstractUserImporter } from './abstract-user-importer.js'
import { JobQueue } from '@server/lib/job-queue/job-queue.js'
import { isValidActorHandle } from '@server/helpers/custom-validators/activitypub/actor.js'
import { pick } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('user-import')
export class FollowingImporter extends AbstractUserImporter <FollowingExportJSON, FollowingExportJSON['following'][0]> {
type SanitizedObject = Pick<FollowingExportJSON['following'][0], 'targetHandle'>
export class FollowingImporter extends AbstractUserImporter <FollowingExportJSON, FollowingExportJSON['following'][0], SanitizedObject> {
protected getImportObjects (json: FollowingExportJSON) {
return json.following
@ -15,10 +18,10 @@ export class FollowingImporter extends AbstractUserImporter <FollowingExportJSON
protected sanitize (followingImportData: FollowingExportJSON['following'][0]) {
if (!isValidActorHandle(followingImportData.targetHandle)) return undefined
return followingImportData
return pick(followingImportData, [ 'targetHandle' ])
}
protected async importObject (followingImportData: FollowingExportJSON['following'][0]) {
protected async importObject (followingImportData: SanitizedObject) {
const [ name, host ] = followingImportData.targetHandle.split('@')
const payload = {

View file

@ -1,5 +1,5 @@
import { LikesExportJSON } from '@peertube/peertube-models'
import { AbstractRatesImporter } from './abstract-rates-importer.js'
import { AbstractRatesImporter, SanitizedRateObject } from './abstract-rates-importer.js'
export class LikesImporter extends AbstractRatesImporter <LikesExportJSON, LikesExportJSON['likes'][0]> {
@ -11,7 +11,7 @@ export class LikesImporter extends AbstractRatesImporter <LikesExportJSON, Likes
return this.sanitizeRate(o)
}
protected async importObject (likesImportData: LikesExportJSON['likes'][0]) {
protected async importObject (likesImportData: SanitizedRateObject) {
return this.importRate(likesImportData, 'like')
}
}

View file

@ -16,10 +16,14 @@ import {
import { isThemeNameValid } from '@server/helpers/custom-validators/plugins.js'
import { isThemeRegistered } from '@server/lib/plugins/theme-utils.js'
import { isUserNotificationSettingValid } from '@server/helpers/custom-validators/user-notifications.js'
import { pick } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('user-import')
export class UserSettingsImporter extends AbstractUserImporter <UserSettingsExportJSON, UserSettingsExportJSON> {
type SanitizedObject = Pick<UserSettingsExportJSON, 'nsfwPolicy' | 'autoPlayVideo' | 'autoPlayNextVideo' | 'autoPlayNextVideo' |
'autoPlayNextVideoPlaylist' | 'p2pEnabled' | 'videosHistoryEnabled' | 'videoLanguages' | 'theme' | 'notificationSettings'>
export class UserSettingsImporter extends AbstractUserImporter <UserSettingsExportJSON, UserSettingsExportJSON, SanitizedObject> {
protected getImportObjects (json: UserSettingsExportJSON) {
return [ json ]
@ -27,7 +31,6 @@ export class UserSettingsImporter extends AbstractUserImporter <UserSettingsExpo
protected sanitize (o: UserSettingsExportJSON) {
if (!isUserNSFWPolicyValid(o.nsfwPolicy)) o.nsfwPolicy = undefined
if (!isUserAutoPlayVideoValid(o.autoPlayVideo)) o.autoPlayVideo = undefined
if (!isUserAutoPlayNextVideoValid(o.autoPlayNextVideo)) o.autoPlayNextVideo = undefined
if (!isUserAutoPlayNextVideoPlaylistValid(o.autoPlayNextVideoPlaylist)) o.autoPlayNextVideoPlaylist = undefined
@ -40,10 +43,20 @@ export class UserSettingsImporter extends AbstractUserImporter <UserSettingsExpo
if (!isUserNotificationSettingValid(o.notificationSettings[key])) (o.notificationSettings[key] as any) = undefined
}
return o
return pick(o, [
'nsfwPolicy',
'autoPlayVideo',
'autoPlayNextVideo',
'autoPlayNextVideoPlaylist',
'p2pEnabled',
'videosHistoryEnabled',
'videoLanguages',
'theme',
'notificationSettings'
])
}
protected async importObject (userImportData: UserSettingsExportJSON) {
protected async importObject (userImportData: SanitizedObject) {
if (exists(userImportData.nsfwPolicy)) this.user.nsfwPolicy = userImportData.nsfwPolicy
if (exists(userImportData.autoPlayVideo)) this.user.autoPlayVideo = userImportData.autoPlayVideo
if (exists(userImportData.autoPlayNextVideo)) this.user.autoPlayNextVideo = userImportData.autoPlayNextVideo

View file

@ -27,16 +27,21 @@ import { isActorPreferredUsernameValid } from '@server/helpers/custom-validators
import { saveInTransactionWithRetries } from '@server/helpers/database-utils.js'
import { isArray } from '@server/helpers/custom-validators/misc.js'
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
import { pick } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('user-import')
export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylistsExportJSON, VideoPlaylistsExportJSON['videoPlaylists'][0]> {
type ImportObject = VideoPlaylistsExportJSON['videoPlaylists'][0]
type SanitizedObject = Pick<ImportObject, 'type' | 'displayName' | 'privacy' | 'elements' | 'description' | 'elements' | 'channel' |
'archiveFiles'>
export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylistsExportJSON, ImportObject, SanitizedObject> {
protected getImportObjects (json: VideoPlaylistsExportJSON) {
return json.videoPlaylists
}
protected sanitize (o: VideoPlaylistsExportJSON['videoPlaylists'][0]) {
protected sanitize (o: ImportObject) {
if (!isVideoPlaylistTypeValid(o.type)) return undefined
if (!isVideoPlaylistNameValid(o.displayName)) return undefined
if (!isVideoPlaylistPrivacyValid(o.privacy)) return undefined
@ -53,10 +58,10 @@ export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylists
return true
})
return o
return pick(o, [ 'type', 'displayName', 'privacy', 'elements', 'channel', 'description', 'archiveFiles' ])
}
protected async importObject (playlistImportData: VideoPlaylistsExportJSON['videoPlaylists'][0]) {
protected async importObject (playlistImportData: SanitizedObject) {
const existingPlaylist = await VideoPlaylistModel.loadRegularByAccountAndName(this.user.Account, playlistImportData.displayName)
if (existingPlaylist) {
@ -77,7 +82,7 @@ export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylists
return { duplicate: false }
}
private async createPlaylist (playlistImportData: VideoPlaylistsExportJSON['videoPlaylists'][0]) {
private async createPlaylist (playlistImportData: SanitizedObject) {
let videoChannel: MChannelBannerAccountDefault
if (playlistImportData.channel.name) {
@ -115,7 +120,7 @@ export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylists
return VideoPlaylistModel.loadWatchLaterOf(this.user.Account)
}
private async createThumbnail (playlist: MVideoPlaylistThumbnail, playlistImportData: VideoPlaylistsExportJSON['videoPlaylists'][0]) {
private async createThumbnail (playlist: MVideoPlaylistThumbnail, playlistImportData: SanitizedObject) {
const thumbnailPath = this.getSafeArchivePathOrThrow(playlistImportData.archiveFiles.thumbnail)
if (!thumbnailPath) return undefined
@ -130,7 +135,7 @@ export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylists
await playlist.setAndSaveThumbnail(thumbnail, undefined)
}
private async createElements (playlist: MVideoPlaylist, playlistImportData: VideoPlaylistsExportJSON['videoPlaylists'][0]) {
private async createElements (playlist: MVideoPlaylist, playlistImportData: SanitizedObject) {
const elementsToCreate: { videoId: number, startTimestamp: number, stopTimestamp: number }[] = []
for (const element of playlistImportData.elements.slice(0, USER_IMPORT.MAX_PLAYLIST_ELEMENTS)) {

View file

@ -35,6 +35,7 @@ import {
isVideoOriginallyPublishedAtValid,
isVideoPrivacyValid,
isVideoReplayPrivacyValid,
isVideoSourceFilenameValid,
isVideoSupportValid,
isVideoTagValid
} from '@server/helpers/custom-validators/videos.js'
@ -50,13 +51,18 @@ import { isLocalVideoFileAccepted } from '@server/lib/moderation.js'
const lTags = loggerTagsFactory('user-import')
export class VideosImporter extends AbstractUserImporter <VideoExportJSON, VideoExportJSON['videos'][0]> {
type ImportObject = VideoExportJSON['videos'][0]
type SanitizedObject = Pick<ImportObject, 'name' | 'duration' | 'channel' | 'privacy' | 'archiveFiles' | 'captions' | 'category' |
'licence' | 'language' | 'description' | 'support' | 'nsfw' | 'isLive' | 'commentsEnabled' | 'downloadEnabled' | 'waitTranscoding' |
'originallyPublishedAt' | 'tags' | 'live' | 'passwords' | 'source'>
export class VideosImporter extends AbstractUserImporter <VideoExportJSON, ImportObject, SanitizedObject> {
protected getImportObjects (json: VideoExportJSON) {
return json.videos
}
protected sanitize (o: VideoExportJSON['videos'][0]) {
protected sanitize (o: ImportObject) {
if (!isVideoNameValid(o.name)) return undefined
if (!isVideoDurationValid(o.duration + '')) return undefined
if (!isVideoChannelUsernameValid(o.channel?.name)) return undefined
@ -75,6 +81,8 @@ export class VideosImporter extends AbstractUserImporter <VideoExportJSON, Video
if (!isBooleanValid(o.downloadEnabled)) o.downloadEnabled = CONFIG.DEFAULTS.PUBLISH.DOWNLOAD_ENABLED
if (!isBooleanValid(o.waitTranscoding)) o.waitTranscoding = true
if (!isVideoSourceFilenameValid(o.source?.filename)) o.source = undefined
if (!isVideoOriginallyPublishedAtValid(o.originallyPublishedAt)) o.originallyPublishedAt = null
if (!isArray(o.tags)) o.tags = []
@ -102,10 +110,32 @@ export class VideosImporter extends AbstractUserImporter <VideoExportJSON, Video
if (o.passwords.some(p => !isPasswordValid(p))) return undefined
}
return o
return pick(o, [
'name',
'duration',
'channel',
'privacy',
'archiveFiles',
'category',
'licence',
'language',
'description',
'support',
'nsfw',
'isLive',
'commentsEnabled',
'downloadEnabled',
'waitTranscoding',
'originallyPublishedAt',
'tags',
'captions',
'live',
'passwords',
'source'
])
}
protected async importObject (videoImportData: VideoExportJSON['videos'][0]) {
protected async importObject (videoImportData: SanitizedObject) {
const videoFilePath = this.getSafeArchivePathOrThrow(videoImportData.archiveFiles.videoFile)
const videoSize = await getFileSize(videoFilePath)
@ -247,7 +277,7 @@ export class VideosImporter extends AbstractUserImporter <VideoExportJSON, Video
return { duplicate: false }
}
private async importCaptions (video: MVideoFullLight, videoImportData: VideoExportJSON['videos'][0]) {
private async importCaptions (video: MVideoFullLight, videoImportData: SanitizedObject) {
const captionPaths: string[] = []
for (const captionImport of videoImportData.captions) {
@ -284,7 +314,7 @@ export class VideosImporter extends AbstractUserImporter <VideoExportJSON, Video
videoFilePath: string
size: number
channel: MChannelId
videoImportData: VideoExportJSON['videos'][0]
videoImportData: SanitizedObject
}) {
const { videoFilePath, size, videoImportData, channel } = options

View file

@ -32,6 +32,7 @@ import {
isVideoNameValid,
isVideoOriginallyPublishedAtValid,
isVideoPrivacyValid,
isVideoSourceFilenameValid,
isVideoSupportValid
} from '../../../helpers/custom-validators/videos.js'
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
@ -133,7 +134,7 @@ const videosAddResumableValidator = [
*/
const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
body('filename')
.exists(),
.custom(isVideoSourceFilenameValid),
body('name')
.trim()
.custom(isVideoNameValid).withMessage(