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 const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
function isVideoIncludeValid (include: VideoIncludeType) { export function isVideoIncludeValid (include: VideoIncludeType) {
return exists(include) && validator.default.isInt('' + include) return exists(include) && validator.default.isInt('' + include)
} }
function isVideoCategoryValid (value: any) { export function isVideoCategoryValid (value: any) {
return value === null || VIDEO_CATEGORIES[value] !== undefined return value === null || VIDEO_CATEGORIES[value] !== undefined
} }
function isVideoStateValid (value: any) { export function isVideoStateValid (value: any) {
return exists(value) && VIDEO_STATES[value] !== undefined return exists(value) && VIDEO_STATES[value] !== undefined
} }
function isVideoLicenceValid (value: any) { export function isVideoLicenceValid (value: any) {
return value === null || VIDEO_LICENCES[value] !== undefined return value === null || VIDEO_LICENCES[value] !== undefined
} }
function isVideoLanguageValid (value: any) { export function isVideoLanguageValid (value: any) {
return value === null || return value === null ||
(typeof value === 'string' && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.LANGUAGE)) (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) 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)) 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)) 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) 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) return exists(tag) && validator.default.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG)
} }
function areVideoTagsValid (tags: string[]) { export function areVideoTagsValid (tags: string[]) {
return tags === null || ( return tags === null || (
isArray(tags) && isArray(tags) &&
validator.default.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.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) return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS)
} }
const ratingTypes = new Set(Object.values(VIDEO_RATE_TYPES)) 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) 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) 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({ return isFileValid({
files, files,
mimeTypeRegex: MIMETYPES.VIDEO.MIMETYPES_REGEX, mimeTypeRegex: MIMETYPES.VIDEO.MIMETYPES_REGEX,
@ -93,7 +97,7 @@ const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
.join('|') .join('|')
const videoImageTypesRegex = `image/(${videoImageTypes})` const videoImageTypesRegex = `image/(${videoImageTypes})`
function isVideoImageValid (files: UploadFilesForCheck, field: string, optional = true) { export function isVideoImageValid (files: UploadFilesForCheck, field: string, optional = true) {
return isFileValid({ return isFileValid({
files, files,
mimeTypeRegex: videoImageTypesRegex, 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 return VIDEO_PRIVACIES[value] !== undefined
} }
function isVideoReplayPrivacyValid (value: number) { export function isVideoReplayPrivacyValid (value: number) {
return VIDEO_PRIVACIES[value] !== undefined && value !== VideoPrivacy.PASSWORD_PROTECTED 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 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) 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) 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 + '') return exists(value) && validator.default.isInt(value + '')
} }
function isVideoFPSResolutionValid (value: string) { export function isVideoFPSResolutionValid (value: string) {
return value === null || validator.default.isInt(value + '') 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) 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 if (!exists(value)) return false
const parsed = magnetUriDecode(value) const parsed = magnetUriDecode(value)
return parsed && isVideoFileInfoHashValid(parsed.infoHash) return parsed && isVideoFileInfoHashValid(parsed.infoHash)
} }
function isPasswordValid (password: string) { export function isPasswordValid (password: string) {
return password.length >= CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.min && return password.length >= CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.min &&
password.length < CONSTRAINTS_FIELDS.VIDEO_PASSWORD.LENGTH.max 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) => { const fail = (message: string) => {
res.fail({ res.fail({
status: HttpStatusCode.BAD_REQUEST_400, status: HttpStatusCode.BAD_REQUEST_400,
@ -184,35 +188,3 @@ function isValidPasswordProtectedPrivacy (req: Request, res: Response) {
return true 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 PARTIAL_UPLOAD_SIZE: { max: 50 * 1024 * 1024 * 1024 }, // 50GB
URL: { min: 3, max: 2000 } // Length URL: { min: 3, max: 2000 } // Length
}, },
VIDEO_SOURCE: {
FILENAME: { min: 1, max: 1000 } // Length
},
VIDEO_PLAYLISTS: { VIDEO_PLAYLISTS: {
NAME: { min: 1, max: 120 }, // Length NAME: { min: 1, max: 120 }, // Length
DESCRIPTION: { min: 3, max: 1000 }, // 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 { userRateVideo } from '@server/lib/rate.js'
import { VideoModel } from '@server/models/video/video.js' import { VideoModel } from '@server/models/video/video.js'
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.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) { protected sanitizeRate <O extends { videoUrl: string }> (data: O) {
if (!isUrlValid(data.videoUrl)) return undefined 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 videoUrl = data.videoUrl
const videoImmutable = await loadOrCreateVideoIfAllowedForUser(videoUrl) const videoImmutable = await loadOrCreateVideoIfAllowedForUser(videoUrl)

View file

@ -7,7 +7,11 @@ import { dirname, resolve } from 'path'
const lTags = loggerTagsFactory('user-import') 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 user: MUserDefault
protected extractedDirectory: string protected extractedDirectory: string
protected jsonFilePath: string protected jsonFilePath: string
@ -78,7 +82,7 @@ export abstract class AbstractUserImporter <E, O extends { archiveFiles?: Record
} }
async import () { async import () {
const importData: E = await readJSON(this.jsonFilePath) const importData: ROOT_OBJECT = await readJSON(this.jsonFilePath)
const summary = { const summary = {
duplicates: 0, duplicates: 0,
success: 0, success: 0,
@ -111,9 +115,9 @@ export abstract class AbstractUserImporter <E, O extends { archiveFiles?: Record
return summary 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 { AccountModel } from '@server/models/account/account.js'
import { isValidActorHandle } from '@server/helpers/custom-validators/activitypub/actor.js' import { isValidActorHandle } from '@server/helpers/custom-validators/activitypub/actor.js'
import { isHostValid } from '@server/helpers/custom-validators/servers.js' import { isHostValid } from '@server/helpers/custom-validators/servers.js'
import { pick } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('user-import') const lTags = loggerTagsFactory('user-import')
type ImportObject = { handle: string | null, host: string | null, archiveFiles?: never } 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) { protected getImportObjects (json: BlocklistExportJSON) {
return [ return [
@ -23,7 +24,7 @@ export class BlocklistImporter extends AbstractUserImporter <BlocklistExportJSON
protected sanitize (blocklistImportData: ImportObject) { protected sanitize (blocklistImportData: ImportObject) {
if (!isValidActorHandle(blocklistImportData.handle) && !isHostValid(blocklistImportData.host)) return undefined if (!isValidActorHandle(blocklistImportData.handle) && !isHostValid(blocklistImportData.host)) return undefined
return blocklistImportData return pick(blocklistImportData, [ 'handle', 'host' ])
} }
protected async importObject (blocklistImportData: ImportObject) { 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 { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
import { MAccountDefault } from '@server/types/models/index.js' import { MAccountDefault } from '@server/types/models/index.js'
import { isUserDescriptionValid, isUserDisplayNameValid } from '@server/helpers/custom-validators/users.js' import { isUserDescriptionValid, isUserDisplayNameValid } from '@server/helpers/custom-validators/users.js'
import { pick } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('user-import') 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) { protected getImportObjects (json: AccountExportJSON) {
return [ json ] return [ json ]
} }
protected sanitize (blocklistImportData: AccountExportJSON) { protected sanitize (blocklistImportData: AccountExportJSON) {
if (!isUserDisplayNameValid(blocklistImportData.name)) return undefined if (!isUserDisplayNameValid(blocklistImportData.displayName)) return undefined
if (!isUserDescriptionValid(blocklistImportData.description)) blocklistImportData.description = null 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 const account = this.user.Account
account.name = accountImportData.displayName account.name = accountImportData.displayName
@ -38,7 +41,7 @@ export class AccountImporter extends AbstractUserImporter <AccountExportJSON, Ac
return { duplicate: false } return { duplicate: false }
} }
private async importAvatar (account: MAccountDefault, accountImportData: AccountExportJSON) { private async importAvatar (account: MAccountDefault, accountImportData: SanitizedObject) {
const avatarPath = this.getSafeArchivePathOrThrow(accountImportData.archiveFiles.avatar) const avatarPath = this.getSafeArchivePathOrThrow(accountImportData.archiveFiles.avatar)
if (!avatarPath) return undefined if (!avatarPath) return undefined

View file

@ -17,23 +17,25 @@ import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
const lTags = loggerTagsFactory('user-import') 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) { protected getImportObjects (json: ChannelExportJSON) {
return json.channels return json.channels
} }
protected sanitize (blocklistImportData: ChannelExportJSON['channels'][0]) { protected sanitize (channelImportData: ChannelExportJSON['channels'][0]) {
if (!isVideoChannelUsernameValid(blocklistImportData.name)) return undefined if (!isVideoChannelUsernameValid(channelImportData.name)) return undefined
if (!isVideoChannelDisplayNameValid(blocklistImportData.name)) return undefined if (!isVideoChannelDisplayNameValid(channelImportData.displayName)) return undefined
if (!isVideoChannelDescriptionValid(blocklistImportData.description)) blocklistImportData.description = null if (!isVideoChannelDescriptionValid(channelImportData.description)) channelImportData.description = null
if (!isVideoChannelSupportValid(blocklistImportData.support)) blocklistImportData.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 account = this.user.Account
const existingChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(channelImportData.name) const existingChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(channelImportData.name)

View file

@ -1,5 +1,5 @@
import { DislikesExportJSON } from '@peertube/peertube-models' 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]> { export class DislikesImporter extends AbstractRatesImporter <DislikesExportJSON, DislikesExportJSON['dislikes'][0]> {
@ -11,7 +11,7 @@ export class DislikesImporter extends AbstractRatesImporter <DislikesExportJSON,
return this.sanitizeRate(o) return this.sanitizeRate(o)
} }
protected async importObject (dislikesImportData: DislikesExportJSON['dislikes'][0]) { protected async importObject (dislikesImportData: SanitizedRateObject) {
return this.importRate(dislikesImportData, 'dislike') 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 { AbstractUserImporter } from './abstract-user-importer.js'
import { JobQueue } from '@server/lib/job-queue/job-queue.js' import { JobQueue } from '@server/lib/job-queue/job-queue.js'
import { isValidActorHandle } from '@server/helpers/custom-validators/activitypub/actor.js' import { isValidActorHandle } from '@server/helpers/custom-validators/activitypub/actor.js'
import { pick } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('user-import') 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) { protected getImportObjects (json: FollowingExportJSON) {
return json.following return json.following
@ -15,10 +18,10 @@ export class FollowingImporter extends AbstractUserImporter <FollowingExportJSON
protected sanitize (followingImportData: FollowingExportJSON['following'][0]) { protected sanitize (followingImportData: FollowingExportJSON['following'][0]) {
if (!isValidActorHandle(followingImportData.targetHandle)) return undefined 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 [ name, host ] = followingImportData.targetHandle.split('@')
const payload = { const payload = {

View file

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

View file

@ -16,10 +16,14 @@ import {
import { isThemeNameValid } from '@server/helpers/custom-validators/plugins.js' import { isThemeNameValid } from '@server/helpers/custom-validators/plugins.js'
import { isThemeRegistered } from '@server/lib/plugins/theme-utils.js' import { isThemeRegistered } from '@server/lib/plugins/theme-utils.js'
import { isUserNotificationSettingValid } from '@server/helpers/custom-validators/user-notifications.js' import { isUserNotificationSettingValid } from '@server/helpers/custom-validators/user-notifications.js'
import { pick } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('user-import') 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) { protected getImportObjects (json: UserSettingsExportJSON) {
return [ json ] return [ json ]
@ -27,7 +31,6 @@ export class UserSettingsImporter extends AbstractUserImporter <UserSettingsExpo
protected sanitize (o: UserSettingsExportJSON) { protected sanitize (o: UserSettingsExportJSON) {
if (!isUserNSFWPolicyValid(o.nsfwPolicy)) o.nsfwPolicy = undefined if (!isUserNSFWPolicyValid(o.nsfwPolicy)) o.nsfwPolicy = undefined
if (!isUserAutoPlayVideoValid(o.autoPlayVideo)) o.autoPlayVideo = undefined if (!isUserAutoPlayVideoValid(o.autoPlayVideo)) o.autoPlayVideo = undefined
if (!isUserAutoPlayNextVideoValid(o.autoPlayNextVideo)) o.autoPlayNextVideo = undefined if (!isUserAutoPlayNextVideoValid(o.autoPlayNextVideo)) o.autoPlayNextVideo = undefined
if (!isUserAutoPlayNextVideoPlaylistValid(o.autoPlayNextVideoPlaylist)) o.autoPlayNextVideoPlaylist = 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 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.nsfwPolicy)) this.user.nsfwPolicy = userImportData.nsfwPolicy
if (exists(userImportData.autoPlayVideo)) this.user.autoPlayVideo = userImportData.autoPlayVideo if (exists(userImportData.autoPlayVideo)) this.user.autoPlayVideo = userImportData.autoPlayVideo
if (exists(userImportData.autoPlayNextVideo)) this.user.autoPlayNextVideo = userImportData.autoPlayNextVideo 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 { saveInTransactionWithRetries } from '@server/helpers/database-utils.js'
import { isArray } from '@server/helpers/custom-validators/misc.js' import { isArray } from '@server/helpers/custom-validators/misc.js'
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js' import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
import { pick } from '@peertube/peertube-core-utils'
const lTags = loggerTagsFactory('user-import') 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) { protected getImportObjects (json: VideoPlaylistsExportJSON) {
return json.videoPlaylists return json.videoPlaylists
} }
protected sanitize (o: VideoPlaylistsExportJSON['videoPlaylists'][0]) { protected sanitize (o: ImportObject) {
if (!isVideoPlaylistTypeValid(o.type)) return undefined if (!isVideoPlaylistTypeValid(o.type)) return undefined
if (!isVideoPlaylistNameValid(o.displayName)) return undefined if (!isVideoPlaylistNameValid(o.displayName)) return undefined
if (!isVideoPlaylistPrivacyValid(o.privacy)) return undefined if (!isVideoPlaylistPrivacyValid(o.privacy)) return undefined
@ -53,10 +58,10 @@ export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylists
return true 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) const existingPlaylist = await VideoPlaylistModel.loadRegularByAccountAndName(this.user.Account, playlistImportData.displayName)
if (existingPlaylist) { if (existingPlaylist) {
@ -77,7 +82,7 @@ export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylists
return { duplicate: false } return { duplicate: false }
} }
private async createPlaylist (playlistImportData: VideoPlaylistsExportJSON['videoPlaylists'][0]) { private async createPlaylist (playlistImportData: SanitizedObject) {
let videoChannel: MChannelBannerAccountDefault let videoChannel: MChannelBannerAccountDefault
if (playlistImportData.channel.name) { if (playlistImportData.channel.name) {
@ -115,7 +120,7 @@ export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylists
return VideoPlaylistModel.loadWatchLaterOf(this.user.Account) 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) const thumbnailPath = this.getSafeArchivePathOrThrow(playlistImportData.archiveFiles.thumbnail)
if (!thumbnailPath) return undefined if (!thumbnailPath) return undefined
@ -130,7 +135,7 @@ export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylists
await playlist.setAndSaveThumbnail(thumbnail, undefined) 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 }[] = [] const elementsToCreate: { videoId: number, startTimestamp: number, stopTimestamp: number }[] = []
for (const element of playlistImportData.elements.slice(0, USER_IMPORT.MAX_PLAYLIST_ELEMENTS)) { for (const element of playlistImportData.elements.slice(0, USER_IMPORT.MAX_PLAYLIST_ELEMENTS)) {

View file

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

View file

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