diff --git a/server/core/helpers/custom-validators/videos.ts b/server/core/helpers/custom-validators/videos.ts index 0dede6c85..f99f93847 100644 --- a/server/core/helpers/custom-validators/videos.ts +++ b/server/core/helpers/custom-validators/videos.ts @@ -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 -} diff --git a/server/core/initializers/constants.ts b/server/core/initializers/constants.ts index 2fee8ff3e..143b586d0 100644 --- a/server/core/initializers/constants.ts +++ b/server/core/initializers/constants.ts @@ -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 diff --git a/server/core/lib/user-import-export/importers/abstract-rates-importer.ts b/server/core/lib/user-import-export/importers/abstract-rates-importer.ts index a836570b6..70697aa3f 100644 --- a/server/core/lib/user-import-export/importers/abstract-rates-importer.ts +++ b/server/core/lib/user-import-export/importers/abstract-rates-importer.ts @@ -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 extends AbstractUserImporter { +export type SanitizedRateObject = { videoUrl: string } + +export abstract class AbstractRatesImporter extends AbstractUserImporter { protected sanitizeRate (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) diff --git a/server/core/lib/user-import-export/importers/abstract-user-importer.ts b/server/core/lib/user-import-export/importers/abstract-user-importer.ts index ae301a689..6f435e0c4 100644 --- a/server/core/lib/user-import-export/importers/abstract-user-importer.ts +++ b/server/core/lib/user-import-export/importers/abstract-user-importer.ts @@ -7,7 +7,11 @@ import { dirname, resolve } from 'path' const lTags = loggerTagsFactory('user-import') -export abstract class AbstractUserImporter > }> { +export abstract class AbstractUserImporter < + ROOT_OBJECT, + OBJECT extends { archiveFiles?: Record> }, + SANITIZED_OBJECT +> { protected user: MUserDefault protected extractedDirectory: string protected jsonFilePath: string @@ -78,7 +82,7 @@ export abstract class AbstractUserImporter + protected abstract importObject (object: SANITIZED_OBJECT): Awaitable<{ duplicate: boolean }> } diff --git a/server/core/lib/user-import-export/importers/account-blocklist-importer.ts b/server/core/lib/user-import-export/importers/account-blocklist-importer.ts index 844ef295a..5a7534eef 100644 --- a/server/core/lib/user-import-export/importers/account-blocklist-importer.ts +++ b/server/core/lib/user-import-export/importers/account-blocklist-importer.ts @@ -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 { +export class BlocklistImporter extends AbstractUserImporter { protected getImportObjects (json: BlocklistExportJSON) { return [ @@ -23,7 +24,7 @@ export class BlocklistImporter extends AbstractUserImporter { +type SanitizedObject = Pick + +export class AccountImporter extends AbstractUserImporter { 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 { +type SanitizedObject = Pick + +export class ChannelsImporter extends AbstractUserImporter { 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) diff --git a/server/core/lib/user-import-export/importers/dislikes-importer.ts b/server/core/lib/user-import-export/importers/dislikes-importer.ts index 064d91e5a..e06b4cfb2 100644 --- a/server/core/lib/user-import-export/importers/dislikes-importer.ts +++ b/server/core/lib/user-import-export/importers/dislikes-importer.ts @@ -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 { @@ -11,7 +11,7 @@ export class DislikesImporter extends AbstractRatesImporter { +type SanitizedObject = Pick + +export class FollowingImporter extends AbstractUserImporter { protected getImportObjects (json: FollowingExportJSON) { return json.following @@ -15,10 +18,10 @@ export class FollowingImporter extends AbstractUserImporter { @@ -11,7 +11,7 @@ export class LikesImporter extends AbstractRatesImporter { +type SanitizedObject = Pick + +export class UserSettingsImporter extends AbstractUserImporter { protected getImportObjects (json: UserSettingsExportJSON) { return [ json ] @@ -27,7 +31,6 @@ export class UserSettingsImporter extends AbstractUserImporter { +type ImportObject = VideoPlaylistsExportJSON['videoPlaylists'][0] +type SanitizedObject = Pick + +export class VideoPlaylistsImporter extends AbstractUserImporter { 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 { +type ImportObject = VideoExportJSON['videos'][0] +type SanitizedObject = Pick + +export class VideosImporter extends AbstractUserImporter { 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 !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