mirror of https://github.com/Chocobozzz/PeerTube
				
				
				
			Stricter import types
Avoid forgetting to sanitize a field by specifying the sanitized object typepull/6266/head
							parent
							
								
									02596be702
								
							
						
					
					
						commit
						009d7b39ac
					
				| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 }>
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue