aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'server/helpers')
-rw-r--r--server/helpers/activitypub.ts65
-rw-r--r--server/helpers/audit-logger.ts10
-rw-r--r--server/helpers/captions-utils.ts2
-rw-r--r--server/helpers/core-utils.ts15
-rw-r--r--server/helpers/custom-jsonld-signature.ts68
-rw-r--r--server/helpers/custom-validators/accounts.ts27
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts3
-rw-r--r--server/helpers/custom-validators/activitypub/actor.ts2
-rw-r--r--server/helpers/custom-validators/activitypub/cache-file.ts12
-rw-r--r--server/helpers/custom-validators/activitypub/misc.ts5
-rw-r--r--server/helpers/custom-validators/activitypub/playlist.ts27
-rw-r--r--server/helpers/custom-validators/activitypub/video-comments.ts4
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts13
-rw-r--r--server/helpers/custom-validators/logs.ts14
-rw-r--r--server/helpers/custom-validators/misc.ts15
-rw-r--r--server/helpers/custom-validators/servers.ts2
-rw-r--r--server/helpers/custom-validators/users.ts9
-rw-r--r--server/helpers/custom-validators/video-abuses.ts6
-rw-r--r--server/helpers/custom-validators/video-blacklist.ts13
-rw-r--r--server/helpers/custom-validators/video-captions.ts6
-rw-r--r--server/helpers/custom-validators/video-channels.ts24
-rw-r--r--server/helpers/custom-validators/video-comments.ts2
-rw-r--r--server/helpers/custom-validators/video-imports.ts6
-rw-r--r--server/helpers/custom-validators/video-playlists.ts55
-rw-r--r--server/helpers/custom-validators/video-rates.ts5
-rw-r--r--server/helpers/custom-validators/videos.ts20
-rw-r--r--server/helpers/custom-validators/webfinger.ts4
-rw-r--r--server/helpers/database-utils.ts5
-rw-r--r--server/helpers/express-utils.ts10
-rw-r--r--server/helpers/ffmpeg-utils.ts140
-rw-r--r--server/helpers/image-utils.ts13
-rw-r--r--server/helpers/logger.ts13
-rw-r--r--server/helpers/peertube-crypto.ts2
-rw-r--r--server/helpers/requests.ts55
-rw-r--r--server/helpers/signup.ts3
-rw-r--r--server/helpers/utils.ts3
-rw-r--r--server/helpers/video.ts4
-rw-r--r--server/helpers/webfinger.ts4
-rw-r--r--server/helpers/webtorrent.ts2
-rw-r--r--server/helpers/youtube-dl.ts33
40 files changed, 557 insertions, 164 deletions
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index f1430055f..951a25669 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -2,7 +2,7 @@ import * as Bluebird from 'bluebird'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { ResultList } from '../../shared/models' 3import { ResultList } from '../../shared/models'
4import { Activity } from '../../shared/models/activitypub' 4import { Activity } from '../../shared/models/activitypub'
5import { ACTIVITY_PUB } from '../initializers' 5import { ACTIVITY_PUB } from '../initializers/constants'
6import { ActorModel } from '../models/activitypub/actor' 6import { ActorModel } from '../models/activitypub/actor'
7import { signJsonLDObject } from './peertube-crypto' 7import { signJsonLDObject } from './peertube-crypto'
8import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
@@ -15,7 +15,7 @@ function activityPubContextify <T> (data: T) {
15 'https://w3id.org/security/v1', 15 'https://w3id.org/security/v1',
16 { 16 {
17 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', 17 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
18 pt: 'https://joinpeertube.org/ns', 18 pt: 'https://joinpeertube.org/ns#',
19 sc: 'http://schema.org#', 19 sc: 'http://schema.org#',
20 Hashtag: 'as:Hashtag', 20 Hashtag: 'as:Hashtag',
21 uuid: 'sc:identifier', 21 uuid: 'sc:identifier',
@@ -24,15 +24,54 @@ function activityPubContextify <T> (data: T) {
24 subtitleLanguage: 'sc:subtitleLanguage', 24 subtitleLanguage: 'sc:subtitleLanguage',
25 sensitive: 'as:sensitive', 25 sensitive: 'as:sensitive',
26 language: 'sc:inLanguage', 26 language: 'sc:inLanguage',
27 views: 'sc:Number',
28 state: 'sc:Number',
29 size: 'sc:Number',
30 fps: 'sc:Number',
31 commentsEnabled: 'sc:Boolean',
32 waitTranscoding: 'sc:Boolean',
33 expires: 'sc:expires', 27 expires: 'sc:expires',
34 support: 'sc:Text', 28 CacheFile: 'pt:CacheFile',
35 CacheFile: 'pt:CacheFile' 29 Infohash: 'pt:Infohash',
30 originallyPublishedAt: 'sc:datePublished',
31 views: {
32 '@type': 'sc:Number',
33 '@id': 'pt:views'
34 },
35 state: {
36 '@type': 'sc:Number',
37 '@id': 'pt:state'
38 },
39 size: {
40 '@type': 'sc:Number',
41 '@id': 'pt:size'
42 },
43 fps: {
44 '@type': 'sc:Number',
45 '@id': 'pt:fps'
46 },
47 startTimestamp: {
48 '@type': 'sc:Number',
49 '@id': 'pt:startTimestamp'
50 },
51 stopTimestamp: {
52 '@type': 'sc:Number',
53 '@id': 'pt:stopTimestamp'
54 },
55 position: {
56 '@type': 'sc:Number',
57 '@id': 'pt:position'
58 },
59 commentsEnabled: {
60 '@type': 'sc:Boolean',
61 '@id': 'pt:commentsEnabled'
62 },
63 downloadEnabled: {
64 '@type': 'sc:Boolean',
65 '@id': 'pt:downloadEnabled'
66 },
67 waitTranscoding: {
68 '@type': 'sc:Boolean',
69 '@id': 'pt:waitTranscoding'
70 },
71 support: {
72 '@type': 'sc:Text',
73 '@id': 'pt:support'
74 }
36 }, 75 },
37 { 76 {
38 likes: { 77 likes: {
@@ -43,6 +82,10 @@ function activityPubContextify <T> (data: T) {
43 '@id': 'as:dislikes', 82 '@id': 'as:dislikes',
44 '@type': '@id' 83 '@type': '@id'
45 }, 84 },
85 playlists: {
86 '@id': 'pt:playlists',
87 '@type': '@id'
88 },
46 shares: { 89 shares: {
47 '@id': 'as:shares', 90 '@id': 'as:shares',
48 '@type': '@id' 91 '@type': '@id'
@@ -64,7 +107,7 @@ async function activityPubCollectionPagination (baseUrl: string, handler: Activi
64 107
65 return { 108 return {
66 id: baseUrl, 109 id: baseUrl,
67 type: 'OrderedCollection', 110 type: 'OrderedCollectionPage',
68 totalItems: result.total, 111 totalItems: result.total,
69 first: baseUrl + '?page=1' 112 first: baseUrl + '?page=1'
70 } 113 }
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts
index 00311fce1..f536da439 100644
--- a/server/helpers/audit-logger.ts
+++ b/server/helpers/audit-logger.ts
@@ -4,15 +4,14 @@ import { diff } from 'deep-object-diff'
4import { chain } from 'lodash' 4import { chain } from 'lodash'
5import * as flatten from 'flat' 5import * as flatten from 'flat'
6import * as winston from 'winston' 6import * as winston from 'winston'
7import { CONFIG } from '../initializers'
8import { jsonLoggerFormat, labelFormatter } from './logger' 7import { jsonLoggerFormat, labelFormatter } from './logger'
9import { VideoDetails, User, VideoChannel, VideoAbuse, VideoImport } from '../../shared' 8import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../../shared'
10import { VideoComment } from '../../shared/models/videos/video-comment.model' 9import { VideoComment } from '../../shared/models/videos/video-comment.model'
11import { CustomConfig } from '../../shared/models/server/custom-config.model' 10import { CustomConfig } from '../../shared/models/server/custom-config.model'
12import { UserModel } from '../models/account/user' 11import { CONFIG } from '../initializers/config'
13 12
14function getAuditIdFromRes (res: express.Response) { 13function getAuditIdFromRes (res: express.Response) {
15 return (res.locals.oauth.token.User as UserModel).username 14 return res.locals.oauth.token.User.username
16} 15}
17 16
18enum AUDIT_TYPE { 17enum AUDIT_TYPE {
@@ -117,7 +116,8 @@ const videoKeysToKeep = [
117 'channel-uuid', 116 'channel-uuid',
118 'channel-name', 117 'channel-name',
119 'support', 118 'support',
120 'commentsEnabled' 119 'commentsEnabled',
120 'downloadEnabled'
121] 121]
122class VideoAuditView extends EntityAuditView { 122class VideoAuditView extends EntityAuditView {
123 constructor (private video: VideoDetails) { 123 constructor (private video: VideoDetails) {
diff --git a/server/helpers/captions-utils.ts b/server/helpers/captions-utils.ts
index 0fb11a125..7174d4654 100644
--- a/server/helpers/captions-utils.ts
+++ b/server/helpers/captions-utils.ts
@@ -1,5 +1,5 @@
1import { join } from 'path' 1import { join } from 'path'
2import { CONFIG } from '../initializers' 2import { CONFIG } from '../initializers/config'
3import { VideoCaptionModel } from '../models/video/video-caption' 3import { VideoCaptionModel } from '../models/video/video-caption'
4import * as srt2vtt from 'srt-to-vtt' 4import * as srt2vtt from 'srt-to-vtt'
5import { createReadStream, createWriteStream, remove, move } from 'fs-extra' 5import { createReadStream, createWriteStream, remove, move } from 'fs-extra'
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 3fb824e36..305d3b71e 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -11,14 +11,13 @@ import * as pem from 'pem'
11import { URL } from 'url' 11import { URL } from 'url'
12import { truncate } from 'lodash' 12import { truncate } from 'lodash'
13import { exec } from 'child_process' 13import { exec } from 'child_process'
14import { isArray } from './custom-validators/misc'
15 14
16const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { 15const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
17 if (!oldObject || typeof oldObject !== 'object') { 16 if (!oldObject || typeof oldObject !== 'object') {
18 return valueConverter(oldObject) 17 return valueConverter(oldObject)
19 } 18 }
20 19
21 if (isArray(oldObject)) { 20 if (Array.isArray(oldObject)) {
22 return oldObject.map(e => objectConverter(e, keyConverter, valueConverter)) 21 return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
23 } 22 }
24 23
@@ -41,7 +40,7 @@ const timeTable = {
41 month: 3600000 * 24 * 30 40 month: 3600000 * 24 * 30
42} 41}
43 42
44export function parseDuration (duration: number | string): number { 43export function parseDurationToMs (duration: number | string): number {
45 if (typeof duration === 'number') return duration 44 if (typeof duration === 'number') return duration
46 45
47 if (typeof duration === 'string') { 46 if (typeof duration === 'string') {
@@ -58,7 +57,7 @@ export function parseDuration (duration: number | string): number {
58 } 57 }
59 } 58 }
60 59
61 throw new Error('Duration could not be properly parsed') 60 throw new Error(`Duration ${duration} could not be properly parsed`)
62} 61}
63 62
64export function parseBytes (value: string | number): number { 63export function parseBytes (value: string | number): number {
@@ -193,10 +192,14 @@ function peertubeTruncate (str: string, maxLength: number) {
193 return truncate(str, options) 192 return truncate(str, options)
194} 193}
195 194
196function sha256 (str: string, encoding: HexBase64Latin1Encoding = 'hex') { 195function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
197 return createHash('sha256').update(str).digest(encoding) 196 return createHash('sha256').update(str).digest(encoding)
198} 197}
199 198
199function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
200 return createHash('sha1').update(str).digest(encoding)
201}
202
200function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { 203function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
201 return function promisified (): Promise<A> { 204 return function promisified (): Promise<A> {
202 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { 205 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
@@ -262,7 +265,9 @@ export {
262 sanitizeHost, 265 sanitizeHost,
263 buildPath, 266 buildPath,
264 peertubeTruncate, 267 peertubeTruncate,
268
265 sha256, 269 sha256,
270 sha1,
266 271
267 promisify0, 272 promisify0,
268 promisify1, 273 promisify1,
diff --git a/server/helpers/custom-jsonld-signature.ts b/server/helpers/custom-jsonld-signature.ts
index 27a187db1..a3bceb047 100644
--- a/server/helpers/custom-jsonld-signature.ts
+++ b/server/helpers/custom-jsonld-signature.ts
@@ -1,13 +1,77 @@
1import * as AsyncLRU from 'async-lru' 1import * as AsyncLRU from 'async-lru'
2import * as jsonld from 'jsonld' 2import * as jsonld from 'jsonld'
3import * as jsig from 'jsonld-signatures' 3import * as jsig from 'jsonld-signatures'
4import { logger } from './logger'
5
6const CACHE = {
7 'https://w3id.org/security/v1': {
8 '@context': {
9 'id': '@id',
10 'type': '@type',
11
12 'dc': 'http://purl.org/dc/terms/',
13 'sec': 'https://w3id.org/security#',
14 'xsd': 'http://www.w3.org/2001/XMLSchema#',
15
16 'EcdsaKoblitzSignature2016': 'sec:EcdsaKoblitzSignature2016',
17 'Ed25519Signature2018': 'sec:Ed25519Signature2018',
18 'EncryptedMessage': 'sec:EncryptedMessage',
19 'GraphSignature2012': 'sec:GraphSignature2012',
20 'LinkedDataSignature2015': 'sec:LinkedDataSignature2015',
21 'LinkedDataSignature2016': 'sec:LinkedDataSignature2016',
22 'CryptographicKey': 'sec:Key',
23
24 'authenticationTag': 'sec:authenticationTag',
25 'canonicalizationAlgorithm': 'sec:canonicalizationAlgorithm',
26 'cipherAlgorithm': 'sec:cipherAlgorithm',
27 'cipherData': 'sec:cipherData',
28 'cipherKey': 'sec:cipherKey',
29 'created': { '@id': 'dc:created', '@type': 'xsd:dateTime' },
30 'creator': { '@id': 'dc:creator', '@type': '@id' },
31 'digestAlgorithm': 'sec:digestAlgorithm',
32 'digestValue': 'sec:digestValue',
33 'domain': 'sec:domain',
34 'encryptionKey': 'sec:encryptionKey',
35 'expiration': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' },
36 'expires': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' },
37 'initializationVector': 'sec:initializationVector',
38 'iterationCount': 'sec:iterationCount',
39 'nonce': 'sec:nonce',
40 'normalizationAlgorithm': 'sec:normalizationAlgorithm',
41 'owner': { '@id': 'sec:owner', '@type': '@id' },
42 'password': 'sec:password',
43 'privateKey': { '@id': 'sec:privateKey', '@type': '@id' },
44 'privateKeyPem': 'sec:privateKeyPem',
45 'publicKey': { '@id': 'sec:publicKey', '@type': '@id' },
46 'publicKeyBase58': 'sec:publicKeyBase58',
47 'publicKeyPem': 'sec:publicKeyPem',
48 'publicKeyWif': 'sec:publicKeyWif',
49 'publicKeyService': { '@id': 'sec:publicKeyService', '@type': '@id' },
50 'revoked': { '@id': 'sec:revoked', '@type': 'xsd:dateTime' },
51 'salt': 'sec:salt',
52 'signature': 'sec:signature',
53 'signatureAlgorithm': 'sec:signingAlgorithm',
54 'signatureValue': 'sec:signatureValue'
55 }
56 }
57}
4 58
5const nodeDocumentLoader = jsonld.documentLoaders.node() 59const nodeDocumentLoader = jsonld.documentLoaders.node()
6 60
7const lru = new AsyncLRU({ 61const lru = new AsyncLRU({
8 max: 10, 62 max: 10,
9 load: (key, cb) => { 63 load: (url, cb) => {
10 nodeDocumentLoader(key, cb) 64 if (CACHE[ url ] !== undefined) {
65 logger.debug('Using cache for JSON-LD %s.', url)
66
67 return cb(null, {
68 contextUrl: null,
69 document: CACHE[ url ],
70 documentUrl: url
71 })
72 }
73
74 nodeDocumentLoader(url, cb)
11 } 75 }
12}) 76})
13 77
diff --git a/server/helpers/custom-validators/accounts.ts b/server/helpers/custom-validators/accounts.ts
index 191de1496..146c7708e 100644
--- a/server/helpers/custom-validators/accounts.ts
+++ b/server/helpers/custom-validators/accounts.ts
@@ -5,7 +5,6 @@ import * as validator from 'validator'
5import { AccountModel } from '../../models/account/account' 5import { AccountModel } from '../../models/account/account'
6import { isUserDescriptionValid, isUserUsernameValid } from './users' 6import { isUserDescriptionValid, isUserUsernameValid } from './users'
7import { exists } from './misc' 7import { exists } from './misc'
8import { CONFIG } from '../../initializers'
9 8
10function isAccountNameValid (value: string) { 9function isAccountNameValid (value: string) {
11 return isUserUsernameValid(value) 10 return isUserUsernameValid(value)
@@ -19,7 +18,7 @@ function isAccountDescriptionValid (value: string) {
19 return isUserDescriptionValid(value) 18 return isUserDescriptionValid(value)
20} 19}
21 20
22function isAccountIdExist (id: number | string, res: Response, sendNotFound = true) { 21function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) {
23 let promise: Bluebird<AccountModel> 22 let promise: Bluebird<AccountModel>
24 23
25 if (validator.isInt('' + id)) { 24 if (validator.isInt('' + id)) {
@@ -28,26 +27,20 @@ function isAccountIdExist (id: number | string, res: Response, sendNotFound = tr
28 promise = AccountModel.loadByUUID('' + id) 27 promise = AccountModel.loadByUUID('' + id)
29 } 28 }
30 29
31 return isAccountExist(promise, res, sendNotFound) 30 return doesAccountExist(promise, res, sendNotFound)
32} 31}
33 32
34function isLocalAccountNameExist (name: string, res: Response, sendNotFound = true) { 33function doesLocalAccountNameExist (name: string, res: Response, sendNotFound = true) {
35 const promise = AccountModel.loadLocalByName(name) 34 const promise = AccountModel.loadLocalByName(name)
36 35
37 return isAccountExist(promise, res, sendNotFound) 36 return doesAccountExist(promise, res, sendNotFound)
38} 37}
39 38
40function isAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) { 39function doesAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) {
41 const [ accountName, host ] = nameWithDomain.split('@') 40 return doesAccountExist(AccountModel.loadByNameWithHost(nameWithDomain), res, sendNotFound)
42
43 let promise: Bluebird<AccountModel>
44 if (!host || host === CONFIG.WEBSERVER.HOST) promise = AccountModel.loadLocalByName(accountName)
45 else promise = AccountModel.loadByNameAndHost(accountName, host)
46
47 return isAccountExist(promise, res, sendNotFound)
48} 41}
49 42
50async function isAccountExist (p: Bluebird<AccountModel>, res: Response, sendNotFound: boolean) { 43async function doesAccountExist (p: Bluebird<AccountModel>, res: Response, sendNotFound: boolean) {
51 const account = await p 44 const account = await p
52 45
53 if (!account) { 46 if (!account) {
@@ -69,9 +62,9 @@ async function isAccountExist (p: Bluebird<AccountModel>, res: Response, sendNot
69 62
70export { 63export {
71 isAccountIdValid, 64 isAccountIdValid,
72 isAccountIdExist, 65 doesAccountIdExist,
73 isLocalAccountNameExist, 66 doesLocalAccountNameExist,
74 isAccountDescriptionValid, 67 isAccountDescriptionValid,
75 isAccountNameWithHostExist, 68 doesAccountNameWithHostExist,
76 isAccountNameValid 69 isAccountNameValid
77} 70}
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index b24590d9d..e0d170d9d 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -9,6 +9,7 @@ import { isViewActivityValid } from './view'
9import { exists } from '../misc' 9import { exists } from '../misc'
10import { isCacheFileObjectValid } from './cache-file' 10import { isCacheFileObjectValid } from './cache-file'
11import { isFlagActivityValid } from './flag' 11import { isFlagActivityValid } from './flag'
12import { isPlaylistObjectValid } from './playlist'
12 13
13function isRootActivityValid (activity: any) { 14function isRootActivityValid (activity: any) {
14 return Array.isArray(activity['@context']) && ( 15 return Array.isArray(activity['@context']) && (
@@ -78,6 +79,7 @@ function checkCreateActivity (activity: any) {
78 isViewActivityValid(activity.object) || 79 isViewActivityValid(activity.object) ||
79 isDislikeActivityValid(activity.object) || 80 isDislikeActivityValid(activity.object) ||
80 isFlagActivityValid(activity.object) || 81 isFlagActivityValid(activity.object) ||
82 isPlaylistObjectValid(activity.object) ||
81 83
82 isCacheFileObjectValid(activity.object) || 84 isCacheFileObjectValid(activity.object) ||
83 sanitizeAndCheckVideoCommentObject(activity.object) || 85 sanitizeAndCheckVideoCommentObject(activity.object) ||
@@ -89,6 +91,7 @@ function checkUpdateActivity (activity: any) {
89 return isBaseActivityValid(activity, 'Update') && 91 return isBaseActivityValid(activity, 'Update') &&
90 ( 92 (
91 isCacheFileObjectValid(activity.object) || 93 isCacheFileObjectValid(activity.object) ||
94 isPlaylistObjectValid(activity.object) ||
92 sanitizeAndCheckVideoTorrentObject(activity.object) || 95 sanitizeAndCheckVideoTorrentObject(activity.object) ||
93 sanitizeAndCheckActorObject(activity.object) 96 sanitizeAndCheckActorObject(activity.object)
94 ) 97 )
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts
index c05f60f14..deb331abb 100644
--- a/server/helpers/custom-validators/activitypub/actor.ts
+++ b/server/helpers/custom-validators/activitypub/actor.ts
@@ -1,5 +1,5 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { CONSTRAINTS_FIELDS } from '../../../initializers' 2import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
3import { exists, isArray } from '../misc' 3import { exists, isArray } from '../misc'
4import { truncate } from 'lodash' 4import { truncate } from 'lodash'
5import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 5import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
diff --git a/server/helpers/custom-validators/activitypub/cache-file.ts b/server/helpers/custom-validators/activitypub/cache-file.ts
index e2bd0c55e..21d5c53ca 100644
--- a/server/helpers/custom-validators/activitypub/cache-file.ts
+++ b/server/helpers/custom-validators/activitypub/cache-file.ts
@@ -8,9 +8,19 @@ function isCacheFileObjectValid (object: CacheFileObject) {
8 object.type === 'CacheFile' && 8 object.type === 'CacheFile' &&
9 isDateValid(object.expires) && 9 isDateValid(object.expires) &&
10 isActivityPubUrlValid(object.object) && 10 isActivityPubUrlValid(object.object) &&
11 isRemoteVideoUrlValid(object.url) 11 (isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
12} 12}
13 13
14// ---------------------------------------------------------------------------
15
14export { 16export {
15 isCacheFileObjectValid 17 isCacheFileObjectValid
16} 18}
19
20// ---------------------------------------------------------------------------
21
22function isPlaylistRedundancyUrlValid (url: any) {
23 return url.type === 'Link' &&
24 (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
25 isActivityPubUrlValid(url.href)
26}
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
index f1762d11c..5afcfbedc 100644
--- a/server/helpers/custom-validators/activitypub/misc.ts
+++ b/server/helpers/custom-validators/activitypub/misc.ts
@@ -1,5 +1,5 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { CONSTRAINTS_FIELDS } from '../../../initializers' 2import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
3import { isTestInstance } from '../../core-utils' 3import { isTestInstance } from '../../core-utils'
4import { exists } from '../misc' 4import { exists } from '../misc'
5 5
@@ -25,8 +25,7 @@ function isActivityPubUrlValid (url: string) {
25} 25}
26 26
27function isBaseActivityValid (activity: any, type: string) { 27function isBaseActivityValid (activity: any, type: string) {
28 return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && 28 return activity.type === type &&
29 activity.type === type &&
30 isActivityPubUrlValid(activity.id) && 29 isActivityPubUrlValid(activity.id) &&
31 isObjectValid(activity.actor) && 30 isObjectValid(activity.actor) &&
32 isUrlCollectionValid(activity.to) && 31 isUrlCollectionValid(activity.to) &&
diff --git a/server/helpers/custom-validators/activitypub/playlist.ts b/server/helpers/custom-validators/activitypub/playlist.ts
new file mode 100644
index 000000000..6c7bdb193
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/playlist.ts
@@ -0,0 +1,27 @@
1import { exists, isDateValid } from '../misc'
2import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
3import * as validator from 'validator'
4import { PlaylistElementObject } from '../../../../shared/models/activitypub/objects/playlist-element-object'
5import { isActivityPubUrlValid } from './misc'
6
7function isPlaylistObjectValid (object: PlaylistObject) {
8 return exists(object) &&
9 object.type === 'Playlist' &&
10 validator.isInt(object.totalItems + '') &&
11 isDateValid(object.published) &&
12 isDateValid(object.updated)
13}
14
15function isPlaylistElementObjectValid (object: PlaylistElementObject) {
16 return exists(object) &&
17 object.type === 'PlaylistElement' &&
18 validator.isInt(object.position + '') &&
19 isActivityPubUrlValid(object.url)
20}
21
22// ---------------------------------------------------------------------------
23
24export {
25 isPlaylistObjectValid,
26 isPlaylistElementObjectValid
27}
diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts
index 0415db21c..26c8c4cc6 100644
--- a/server/helpers/custom-validators/activitypub/video-comments.ts
+++ b/server/helpers/custom-validators/activitypub/video-comments.ts
@@ -1,7 +1,7 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' 2import { ACTIVITY_PUB } from '../../../initializers/constants'
3import { exists, isArray, isDateValid } from '../misc' 3import { exists, isArray, isDateValid } from '../misc'
4import { isActivityPubUrlValid, isBaseActivityValid } from './misc' 4import { isActivityPubUrlValid } from './misc'
5 5
6function sanitizeAndCheckVideoCommentObject (comment: any) { 6function sanitizeAndCheckVideoCommentObject (comment: any) {
7 if (!comment || comment.type !== 'Note') return false 7 if (!comment || comment.type !== 'Note') return false
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index 0f34aab21..3ba6b0744 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -1,7 +1,7 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' 2import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
3import { peertubeTruncate } from '../../core-utils' 3import { peertubeTruncate } from '../../core-utils'
4import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' 4import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
5import { 5import {
6 isVideoDurationValid, 6 isVideoDurationValid,
7 isVideoNameValid, 7 isVideoNameValid,
@@ -12,7 +12,6 @@ import {
12} from '../videos' 12} from '../videos'
13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
14import { VideoState } from '../../../../shared/models/videos' 14import { VideoState } from '../../../../shared/models/videos'
15import { isVideoAbuseReasonValid } from '../video-abuses'
16 15
17function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { 16function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
18 return isBaseActivityValid(activity, 'Update') && 17 return isBaseActivityValid(activity, 'Update') &&
@@ -40,6 +39,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
40 // Default attributes 39 // Default attributes
41 if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED 40 if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
42 if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false 41 if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
42 if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true
43 43
44 return isActivityPubUrlValid(video.id) && 44 return isActivityPubUrlValid(video.id) &&
45 isVideoNameValid(video.name) && 45 isVideoNameValid(video.name) &&
@@ -51,8 +51,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
51 isVideoViewsValid(video.views) && 51 isVideoViewsValid(video.views) &&
52 isBooleanValid(video.sensitive) && 52 isBooleanValid(video.sensitive) &&
53 isBooleanValid(video.commentsEnabled) && 53 isBooleanValid(video.commentsEnabled) &&
54 isBooleanValid(video.downloadEnabled) &&
54 isDateValid(video.published) && 55 isDateValid(video.published) &&
55 isDateValid(video.updated) && 56 isDateValid(video.updated) &&
57 (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
56 (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) && 58 (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
57 isRemoteVideoIconValid(video.icon) && 59 isRemoteVideoIconValid(video.icon) &&
58 video.url.length !== 0 && 60 video.url.length !== 0 &&
@@ -81,6 +83,11 @@ function isRemoteVideoUrlValid (url: any) {
81 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 && 83 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 &&
82 validator.isLength(url.href, { min: 5 }) && 84 validator.isLength(url.href, { min: 5 }) &&
83 validator.isInt(url.height + '', { min: 0 }) 85 validator.isInt(url.height + '', { min: 0 })
86 ) ||
87 (
88 (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
89 isActivityPubUrlValid(url.href) &&
90 isArray(url.tag)
84 ) 91 )
85} 92}
86 93
diff --git a/server/helpers/custom-validators/logs.ts b/server/helpers/custom-validators/logs.ts
new file mode 100644
index 000000000..30d0ce262
--- /dev/null
+++ b/server/helpers/custom-validators/logs.ts
@@ -0,0 +1,14 @@
1import { exists } from './misc'
2import { LogLevel } from '../../../shared/models/server/log-level.type'
3
4const logLevels: LogLevel[] = [ 'debug', 'info', 'warn', 'error' ]
5
6function isValidLogLevel (value: any) {
7 return exists(value) && logLevels.indexOf(value) !== -1
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 isValidLogLevel
14}
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts
index b6f0ebe6f..3a3deab0c 100644
--- a/server/helpers/custom-validators/misc.ts
+++ b/server/helpers/custom-validators/misc.ts
@@ -13,6 +13,10 @@ function isNotEmptyIntArray (value: any) {
13 return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0 13 return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0
14} 14}
15 15
16function isArrayOf (value: any, validator: (value: any) => boolean) {
17 return isArray(value) && value.every(v => validator(v))
18}
19
16function isDateValid (value: string) { 20function isDateValid (value: string) {
17 return exists(value) && validator.isISO8601(value) 21 return exists(value) && validator.isISO8601(value)
18} 22}
@@ -45,12 +49,19 @@ function toValueOrNull (value: string) {
45 return value 49 return value
46} 50}
47 51
48function toArray (value: string) { 52function toArray (value: any) {
49 if (value && isArray(value) === false) return [ value ] 53 if (value && isArray(value) === false) return [ value ]
50 54
51 return value 55 return value
52} 56}
53 57
58function toIntArray (value: any) {
59 if (!value) return []
60 if (isArray(value) === false) return [ validator.toInt(value) ]
61
62 return value.map(v => validator.toInt(v))
63}
64
54function isFileValid ( 65function isFileValid (
55 files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], 66 files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[],
56 mimeTypeRegex: string, 67 mimeTypeRegex: string,
@@ -82,6 +93,7 @@ function isFileValid (
82 93
83export { 94export {
84 exists, 95 exists,
96 isArrayOf,
85 isNotEmptyIntArray, 97 isNotEmptyIntArray,
86 isArray, 98 isArray,
87 isIdValid, 99 isIdValid,
@@ -92,5 +104,6 @@ export {
92 isBooleanValid, 104 isBooleanValid,
93 toIntOrNull, 105 toIntOrNull,
94 toArray, 106 toArray,
107 toIntArray,
95 isFileValid 108 isFileValid
96} 109}
diff --git a/server/helpers/custom-validators/servers.ts b/server/helpers/custom-validators/servers.ts
index 18c80ec8f..5c8bf0d2d 100644
--- a/server/helpers/custom-validators/servers.ts
+++ b/server/helpers/custom-validators/servers.ts
@@ -3,7 +3,7 @@ import 'express-validator'
3 3
4import { isArray, exists } from './misc' 4import { isArray, exists } from './misc'
5import { isTestInstance } from '../core-utils' 5import { isTestInstance } from '../core-utils'
6import { CONSTRAINTS_FIELDS } from '../../initializers' 6import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
7 7
8function isHostValid (host: string) { 8function isHostValid (host: string) {
9 const isURLOptions = { 9 const isURLOptions = {
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index 80652b479..56bc10b16 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -1,8 +1,8 @@
1import 'express-validator' 1import 'express-validator'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { UserRole } from '../../../shared' 3import { UserRole } from '../../../shared'
4import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers' 4import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
5import { exists, isFileValid, isBooleanValid } from './misc' 5import { exists, isBooleanValid, isFileValid } from './misc'
6import { values } from 'lodash' 6import { values } from 'lodash'
7 7
8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS 8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
@@ -54,6 +54,10 @@ function isUserAutoPlayVideoValid (value: any) {
54 return isBooleanValid(value) 54 return isBooleanValid(value)
55} 55}
56 56
57function isUserAdminFlagsValid (value: any) {
58 return exists(value) && validator.isInt('' + value)
59}
60
57function isUserBlockedValid (value: any) { 61function isUserBlockedValid (value: any) {
58 return isBooleanValid(value) 62 return isBooleanValid(value)
59} 63}
@@ -85,6 +89,7 @@ export {
85 isUserVideoQuotaValid, 89 isUserVideoQuotaValid,
86 isUserVideoQuotaDailyValid, 90 isUserVideoQuotaDailyValid,
87 isUserUsernameValid, 91 isUserUsernameValid,
92 isUserAdminFlagsValid,
88 isUserEmailVerifiedValid, 93 isUserEmailVerifiedValid,
89 isUserNSFWPolicyValid, 94 isUserNSFWPolicyValid,
90 isUserWebTorrentEnabledValid, 95 isUserWebTorrentEnabledValid,
diff --git a/server/helpers/custom-validators/video-abuses.ts b/server/helpers/custom-validators/video-abuses.ts
index 290efb149..a61dcee1c 100644
--- a/server/helpers/custom-validators/video-abuses.ts
+++ b/server/helpers/custom-validators/video-abuses.ts
@@ -1,6 +1,6 @@
1import { Response } from 'express' 1import { Response } from 'express'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers' 3import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
4import { exists } from './misc' 4import { exists } from './misc'
5import { VideoAbuseModel } from '../../models/video/video-abuse' 5import { VideoAbuseModel } from '../../models/video/video-abuse'
6 6
@@ -18,7 +18,7 @@ function isVideoAbuseStateValid (value: string) {
18 return exists(value) && VIDEO_ABUSE_STATES[ value ] !== undefined 18 return exists(value) && VIDEO_ABUSE_STATES[ value ] !== undefined
19} 19}
20 20
21async function isVideoAbuseExist (abuseId: number, videoId: number, res: Response) { 21async function doesVideoAbuseExist (abuseId: number, videoId: number, res: Response) {
22 const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId) 22 const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId)
23 23
24 if (videoAbuse === null) { 24 if (videoAbuse === null) {
@@ -36,7 +36,7 @@ async function isVideoAbuseExist (abuseId: number, videoId: number, res: Respons
36// --------------------------------------------------------------------------- 36// ---------------------------------------------------------------------------
37 37
38export { 38export {
39 isVideoAbuseExist, 39 doesVideoAbuseExist,
40 isVideoAbuseStateValid, 40 isVideoAbuseStateValid,
41 isVideoAbuseReasonValid, 41 isVideoAbuseReasonValid,
42 isVideoAbuseModerationCommentValid 42 isVideoAbuseModerationCommentValid
diff --git a/server/helpers/custom-validators/video-blacklist.ts b/server/helpers/custom-validators/video-blacklist.ts
index b36b08d8b..3743f7023 100644
--- a/server/helpers/custom-validators/video-blacklist.ts
+++ b/server/helpers/custom-validators/video-blacklist.ts
@@ -1,7 +1,9 @@
1import { Response } from 'express' 1import { Response } from 'express'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { CONSTRAINTS_FIELDS } from '../../initializers' 3import { exists } from './misc'
4import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
4import { VideoBlacklistModel } from '../../models/video/video-blacklist' 5import { VideoBlacklistModel } from '../../models/video/video-blacklist'
6import { VideoBlacklistType } from '../../../shared/models/videos'
5 7
6const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST 8const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST
7 9
@@ -9,7 +11,7 @@ function isVideoBlacklistReasonValid (value: string) {
9 return value === null || validator.isLength(value, VIDEO_BLACKLIST_CONSTRAINTS_FIELDS.REASON) 11 return value === null || validator.isLength(value, VIDEO_BLACKLIST_CONSTRAINTS_FIELDS.REASON)
10} 12}
11 13
12async function isVideoBlacklistExist (videoId: number, res: Response) { 14async function doesVideoBlacklistExist (videoId: number, res: Response) {
13 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId) 15 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
14 16
15 if (videoBlacklist === null) { 17 if (videoBlacklist === null) {
@@ -24,9 +26,14 @@ async function isVideoBlacklistExist (videoId: number, res: Response) {
24 return true 26 return true
25} 27}
26 28
29function isVideoBlacklistTypeValid (value: any) {
30 return exists(value) && validator.isInt('' + value) && VideoBlacklistType[value] !== undefined
31}
32
27// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
28 34
29export { 35export {
30 isVideoBlacklistReasonValid, 36 isVideoBlacklistReasonValid,
31 isVideoBlacklistExist 37 isVideoBlacklistTypeValid,
38 doesVideoBlacklistExist
32} 39}
diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts
index b33d90e18..3b6569a8a 100644
--- a/server/helpers/custom-validators/video-captions.ts
+++ b/server/helpers/custom-validators/video-captions.ts
@@ -1,4 +1,4 @@
1import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers' 1import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers/constants'
2import { exists, isFileValid } from './misc' 2import { exists, isFileValid } from './misc'
3import { Response } from 'express' 3import { Response } from 'express'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
@@ -16,7 +16,7 @@ function isVideoCaptionFile (files: { [ fieldname: string ]: Express.Multer.File
16 return isFileValid(files, videoCaptionTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max) 16 return isFileValid(files, videoCaptionTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max)
17} 17}
18 18
19async function isVideoCaptionExist (video: VideoModel, language: string, res: Response) { 19async function doesVideoCaptionExist (video: VideoModel, language: string, res: Response) {
20 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language) 20 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language)
21 21
22 if (!videoCaption) { 22 if (!videoCaption) {
@@ -36,5 +36,5 @@ async function isVideoCaptionExist (video: VideoModel, language: string, res: Re
36export { 36export {
37 isVideoCaptionFile, 37 isVideoCaptionFile,
38 isVideoCaptionLanguageValid, 38 isVideoCaptionLanguageValid,
39 isVideoCaptionExist 39 doesVideoCaptionExist
40} 40}
diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts
index f13519c1d..fd56b9a70 100644
--- a/server/helpers/custom-validators/video-channels.ts
+++ b/server/helpers/custom-validators/video-channels.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import 'multer' 3import 'multer'
4import * as validator from 'validator' 4import * as validator from 'validator'
5import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' 5import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
6import { VideoChannelModel } from '../../models/video/video-channel' 6import { VideoChannelModel } from '../../models/video/video-channel'
7import { exists } from './misc' 7import { exists } from './misc'
8 8
@@ -20,29 +20,25 @@ function isVideoChannelSupportValid (value: string) {
20 return value === null || (exists(value) && validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.SUPPORT)) 20 return value === null || (exists(value) && validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.SUPPORT))
21} 21}
22 22
23async function isLocalVideoChannelNameExist (name: string, res: express.Response) { 23async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
24 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) 24 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
25 25
26 return processVideoChannelExist(videoChannel, res) 26 return processVideoChannelExist(videoChannel, res)
27} 27}
28 28
29async function isVideoChannelIdExist (id: string, res: express.Response) { 29async function doesVideoChannelIdExist (id: number | string, res: express.Response) {
30 let videoChannel: VideoChannelModel 30 let videoChannel: VideoChannelModel
31 if (validator.isInt(id)) { 31 if (validator.isInt('' + id)) {
32 videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id) 32 videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
33 } else { // UUID 33 } else { // UUID
34 videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount(id) 34 videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount('' + id)
35 } 35 }
36 36
37 return processVideoChannelExist(videoChannel, res) 37 return processVideoChannelExist(videoChannel, res)
38} 38}
39 39
40async function isVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) { 40async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
41 const [ name, host ] = nameWithDomain.split('@') 41 const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
42 let videoChannel: VideoChannelModel
43
44 if (!host || host === CONFIG.WEBSERVER.HOST) videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
45 else videoChannel = await VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
46 42
47 return processVideoChannelExist(videoChannel, res) 43 return processVideoChannelExist(videoChannel, res)
48} 44}
@@ -50,12 +46,12 @@ async function isVideoChannelNameWithHostExist (nameWithDomain: string, res: exp
50// --------------------------------------------------------------------------- 46// ---------------------------------------------------------------------------
51 47
52export { 48export {
53 isVideoChannelNameWithHostExist, 49 doesVideoChannelNameWithHostExist,
54 isLocalVideoChannelNameExist, 50 doesLocalVideoChannelNameExist,
55 isVideoChannelDescriptionValid, 51 isVideoChannelDescriptionValid,
56 isVideoChannelNameValid, 52 isVideoChannelNameValid,
57 isVideoChannelSupportValid, 53 isVideoChannelSupportValid,
58 isVideoChannelIdExist 54 doesVideoChannelIdExist
59} 55}
60 56
61function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) { 57function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) {
diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts
index 2b3f66063..0707e2af2 100644
--- a/server/helpers/custom-validators/video-comments.ts
+++ b/server/helpers/custom-validators/video-comments.ts
@@ -1,7 +1,7 @@
1import 'express-validator' 1import 'express-validator'
2import 'multer' 2import 'multer'
3import * as validator from 'validator' 3import * as validator from 'validator'
4import { CONSTRAINTS_FIELDS } from '../../initializers' 4import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
5 5
6const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS 6const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS
7 7
diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts
index ce9e9193c..f4235e2fa 100644
--- a/server/helpers/custom-validators/video-imports.ts
+++ b/server/helpers/custom-validators/video-imports.ts
@@ -1,7 +1,7 @@
1import 'express-validator' 1import 'express-validator'
2import 'multer' 2import 'multer'
3import * as validator from 'validator' 3import * as validator from 'validator'
4import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers' 4import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers/constants'
5import { exists, isFileValid } from './misc' 5import { exists, isFileValid } from './misc'
6import * as express from 'express' 6import * as express from 'express'
7import { VideoImportModel } from '../../models/video/video-import' 7import { VideoImportModel } from '../../models/video/video-import'
@@ -30,7 +30,7 @@ function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multe
30 return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true) 30 return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true)
31} 31}
32 32
33async function isVideoImportExist (id: number, res: express.Response) { 33async function doesVideoImportExist (id: number, res: express.Response) {
34 const videoImport = await VideoImportModel.loadAndPopulateVideo(id) 34 const videoImport = await VideoImportModel.loadAndPopulateVideo(id)
35 35
36 if (!videoImport) { 36 if (!videoImport) {
@@ -50,6 +50,6 @@ async function isVideoImportExist (id: number, res: express.Response) {
50export { 50export {
51 isVideoImportStateValid, 51 isVideoImportStateValid,
52 isVideoImportTargetUrlValid, 52 isVideoImportTargetUrlValid,
53 isVideoImportExist, 53 doesVideoImportExist,
54 isVideoImportTorrentFile 54 isVideoImportTorrentFile
55} 55}
diff --git a/server/helpers/custom-validators/video-playlists.ts b/server/helpers/custom-validators/video-playlists.ts
new file mode 100644
index 000000000..2fe426560
--- /dev/null
+++ b/server/helpers/custom-validators/video-playlists.ts
@@ -0,0 +1,55 @@
1import { exists } from './misc'
2import * as validator from 'validator'
3import { CONSTRAINTS_FIELDS, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PLAYLIST_TYPES } from '../../initializers/constants'
4import * as express from 'express'
5import { VideoPlaylistModel } from '../../models/video/video-playlist'
6
7const PLAYLISTS_CONSTRAINT_FIELDS = CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS
8
9function isVideoPlaylistNameValid (value: any) {
10 return exists(value) && validator.isLength(value, PLAYLISTS_CONSTRAINT_FIELDS.NAME)
11}
12
13function isVideoPlaylistDescriptionValid (value: any) {
14 return value === null || (exists(value) && validator.isLength(value, PLAYLISTS_CONSTRAINT_FIELDS.DESCRIPTION))
15}
16
17function isVideoPlaylistPrivacyValid (value: number) {
18 return validator.isInt(value + '') && VIDEO_PLAYLIST_PRIVACIES[ value ] !== undefined
19}
20
21function isVideoPlaylistTimestampValid (value: any) {
22 return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
23}
24
25function isVideoPlaylistTypeValid (value: any) {
26 return exists(value) && VIDEO_PLAYLIST_TYPES[ value ] !== undefined
27}
28
29async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: 'summary' | 'all' = 'summary') {
30 const videoPlaylist = fetchType === 'summary'
31 ? await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined)
32 : await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
33
34 if (!videoPlaylist) {
35 res.status(404)
36 .json({ error: 'Video playlist not found' })
37 .end()
38
39 return false
40 }
41
42 res.locals.videoPlaylist = videoPlaylist
43 return true
44}
45
46// ---------------------------------------------------------------------------
47
48export {
49 doesVideoPlaylistExist,
50 isVideoPlaylistNameValid,
51 isVideoPlaylistDescriptionValid,
52 isVideoPlaylistPrivacyValid,
53 isVideoPlaylistTimestampValid,
54 isVideoPlaylistTypeValid
55}
diff --git a/server/helpers/custom-validators/video-rates.ts b/server/helpers/custom-validators/video-rates.ts
new file mode 100644
index 000000000..f2b6f7cae
--- /dev/null
+++ b/server/helpers/custom-validators/video-rates.ts
@@ -0,0 +1,5 @@
1function isRatingValid (value: any) {
2 return value === 'like' || value === 'dislike'
3}
4
5export { isRatingValid }
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index 95e256b8f..214db17a1 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -5,15 +5,16 @@ import 'multer'
5import * as validator from 'validator' 5import * as validator from 'validator'
6import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' 6import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
7import { 7import {
8 CONSTRAINTS_FIELDS, MIMETYPES, 8 CONSTRAINTS_FIELDS,
9 MIMETYPES,
9 VIDEO_CATEGORIES, 10 VIDEO_CATEGORIES,
10 VIDEO_LICENCES, 11 VIDEO_LICENCES,
11 VIDEO_PRIVACIES, 12 VIDEO_PRIVACIES,
12 VIDEO_RATE_TYPES, 13 VIDEO_RATE_TYPES,
13 VIDEO_STATES 14 VIDEO_STATES
14} from '../../initializers' 15} from '../../initializers/constants'
15import { VideoModel } from '../../models/video/video' 16import { VideoModel } from '../../models/video/video'
16import { exists, isArray, isFileValid } from './misc' 17import { exists, isArray, isDateValid, isFileValid } from './misc'
17import { VideoChannelModel } from '../../models/video/video-channel' 18import { VideoChannelModel } from '../../models/video/video-channel'
18import { UserModel } from '../../models/account/user' 19import { UserModel } from '../../models/account/user'
19import * as magnetUtil from 'magnet-uri' 20import * as magnetUtil from 'magnet-uri'
@@ -115,6 +116,10 @@ function isScheduleVideoUpdatePrivacyValid (value: number) {
115 ) 116 )
116} 117}
117 118
119function isVideoOriginallyPublishedAtValid (value: string | null) {
120 return value === null || isDateValid(value)
121}
122
118function isVideoFileInfoHashValid (value: string | null | undefined) { 123function isVideoFileInfoHashValid (value: string | null | undefined) {
119 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH) 124 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
120} 125}
@@ -161,7 +166,7 @@ function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: Use
161 return true 166 return true
162} 167}
163 168
164async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') { 169async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') {
165 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined 170 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
166 171
167 const video = await fetchVideo(id, fetchType, userId) 172 const video = await fetchVideo(id, fetchType, userId)
@@ -178,7 +183,7 @@ async function isVideoExist (id: string, res: Response, fetchType: VideoFetchTyp
178 return true 183 return true
179} 184}
180 185
181async function isVideoChannelOfAccountExist (channelId: number, user: UserModel, res: Response) { 186async function doesVideoChannelOfAccountExist (channelId: number, user: UserModel, res: Response) {
182 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) { 187 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
183 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) 188 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
184 if (videoChannel === null) { 189 if (videoChannel === null) {
@@ -220,6 +225,7 @@ export {
220 isVideoTagsValid, 225 isVideoTagsValid,
221 isVideoFPSResolutionValid, 226 isVideoFPSResolutionValid,
222 isScheduleVideoUpdatePrivacyValid, 227 isScheduleVideoUpdatePrivacyValid,
228 isVideoOriginallyPublishedAtValid,
223 isVideoFile, 229 isVideoFile,
224 isVideoMagnetUriValid, 230 isVideoMagnetUriValid,
225 isVideoStateValid, 231 isVideoStateValid,
@@ -231,9 +237,9 @@ export {
231 isVideoPrivacyValid, 237 isVideoPrivacyValid,
232 isVideoFileResolutionValid, 238 isVideoFileResolutionValid,
233 isVideoFileSizeValid, 239 isVideoFileSizeValid,
234 isVideoExist, 240 doesVideoExist,
235 isVideoImage, 241 isVideoImage,
236 isVideoChannelOfAccountExist, 242 doesVideoChannelOfAccountExist,
237 isVideoSupportValid, 243 isVideoSupportValid,
238 isVideoFilterValid 244 isVideoFilterValid
239} 245}
diff --git a/server/helpers/custom-validators/webfinger.ts b/server/helpers/custom-validators/webfinger.ts
index 80a7e4a9d..dd914341e 100644
--- a/server/helpers/custom-validators/webfinger.ts
+++ b/server/helpers/custom-validators/webfinger.ts
@@ -1,4 +1,4 @@
1import { CONFIG, REMOTE_SCHEME } from '../../initializers' 1import { REMOTE_SCHEME, WEBSERVER } from '../../initializers/constants'
2import { sanitizeHost } from '../core-utils' 2import { sanitizeHost } from '../core-utils'
3import { exists } from './misc' 3import { exists } from './misc'
4 4
@@ -11,7 +11,7 @@ function isWebfingerLocalResourceValid (value: string) {
11 if (actorParts.length !== 2) return false 11 if (actorParts.length !== 2) return false
12 12
13 const host = actorParts[1] 13 const host = actorParts[1]
14 return sanitizeHost(host, REMOTE_SCHEME.HTTP) === CONFIG.WEBSERVER.HOST 14 return sanitizeHost(host, REMOTE_SCHEME.HTTP) === WEBSERVER.HOST
15} 15}
16 16
17// --------------------------------------------------------------------------- 17// ---------------------------------------------------------------------------
diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts
index 1005d2cf1..39c74b2fd 100644
--- a/server/helpers/database-utils.ts
+++ b/server/helpers/database-utils.ts
@@ -62,14 +62,13 @@ function updateInstanceWithAnother <T extends Model<T>> (instanceToUpdate: Model
62 const obj = baseInstance.toJSON() 62 const obj = baseInstance.toJSON()
63 63
64 for (const key of Object.keys(obj)) { 64 for (const key of Object.keys(obj)) {
65 instanceToUpdate.set(key, obj[key]) 65 instanceToUpdate[key] = obj[key]
66 } 66 }
67} 67}
68 68
69function resetSequelizeInstance (instance: Model<any>, savedFields: object) { 69function resetSequelizeInstance (instance: Model<any>, savedFields: object) {
70 Object.keys(savedFields).forEach(key => { 70 Object.keys(savedFields).forEach(key => {
71 const value = savedFields[key] 71 instance[key] = savedFields[key]
72 instance.set(key, value)
73 }) 72 })
74} 73}
75 74
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
index 9a72ee96d..e0a1d56a5 100644
--- a/server/helpers/express-utils.ts
+++ b/server/helpers/express-utils.ts
@@ -1,11 +1,11 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as multer from 'multer' 2import * as multer from 'multer'
3import { CONFIG, REMOTE_SCHEME } from '../initializers' 3import { REMOTE_SCHEME } from '../initializers/constants'
4import { logger } from './logger' 4import { logger } from './logger'
5import { deleteFileAsync, generateRandomString } from './utils' 5import { deleteFileAsync, generateRandomString } from './utils'
6import { extname } from 'path' 6import { extname } from 'path'
7import { isArray } from './custom-validators/misc' 7import { isArray } from './custom-validators/misc'
8import { UserModel } from '../models/account/user' 8import { CONFIG } from '../initializers/config'
9 9
10function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { 10function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
11 if (paramNSFW === 'true') return true 11 if (paramNSFW === 'true') return true
@@ -13,7 +13,7 @@ function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
13 if (paramNSFW === 'both') return undefined 13 if (paramNSFW === 'both') return undefined
14 14
15 if (res && res.locals.oauth) { 15 if (res && res.locals.oauth) {
16 const user: UserModel = res.locals.oauth.token.User 16 const user = res.locals.oauth.token.User
17 17
18 // User does not want NSFW videos 18 // User does not want NSFW videos
19 if (user.nsfwPolicy === 'do_not_list') return false 19 if (user.nsfwPolicy === 'do_not_list') return false
@@ -59,7 +59,7 @@ function getHostWithPort (host: string) {
59 return host 59 return host
60} 60}
61 61
62function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) { 62function badRequest (req: express.Request, res: express.Response) {
63 return res.type('json').status(400).end() 63 return res.type('json').status(400).end()
64} 64}
65 65
@@ -100,7 +100,7 @@ function createReqFiles (
100} 100}
101 101
102function isUserAbleToSearchRemoteURI (res: express.Response) { 102function isUserAbleToSearchRemoteURI (res: express.Response) {
103 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined 103 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
104 104
105 return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || 105 return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
106 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) 106 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 132f4690e..76b744de8 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,11 +1,12 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { join } from 'path' 2import { dirname, join } from 'path'
3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' 3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' 4import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { logger } from './logger' 6import { logger } from './logger'
7import { checkFFmpegEncoders } from '../initializers/checker-before-init' 7import { checkFFmpegEncoders } from '../initializers/checker-before-init'
8import { remove } from 'fs-extra' 8import { readFile, remove, writeFile } from 'fs-extra'
9import { CONFIG } from '../initializers/config'
9 10
10function computeResolutionsToTranscode (videoFileHeight: number) { 11function computeResolutionsToTranscode (videoFileHeight: number) {
11 const resolutionsEnabled: number[] = [] 12 const resolutionsEnabled: number[] = []
@@ -29,12 +30,21 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
29 return resolutionsEnabled 30 return resolutionsEnabled
30} 31}
31 32
32async function getVideoFileResolution (path: string) { 33async function getVideoFileSize (path: string) {
33 const videoStream = await getVideoFileStream(path) 34 const videoStream = await getVideoFileStream(path)
34 35
35 return { 36 return {
36 videoFileResolution: Math.min(videoStream.height, videoStream.width), 37 width: videoStream.width,
37 isPortraitMode: videoStream.height > videoStream.width 38 height: videoStream.height
39 }
40}
41
42async function getVideoFileResolution (path: string) {
43 const size = await getVideoFileSize(path)
44
45 return {
46 videoFileResolution: Math.min(size.height, size.width),
47 isPortraitMode: size.height > size.width
38 } 48 }
39} 49}
40 50
@@ -95,7 +105,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
95 }) 105 })
96 106
97 const destination = join(folder, imageName) 107 const destination = join(folder, imageName)
98 await processImage({ path: pendingImagePath }, destination, size) 108 await processImage(pendingImagePath, destination, size)
99 } catch (err) { 109 } catch (err) {
100 logger.error('Cannot generate image from video %s.', fromPath, { err }) 110 logger.error('Cannot generate image from video %s.', fromPath, { err })
101 111
@@ -110,52 +120,41 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
110type TranscodeOptions = { 120type TranscodeOptions = {
111 inputPath: string 121 inputPath: string
112 outputPath: string 122 outputPath: string
113 resolution?: VideoResolution 123 resolution: VideoResolution
114 isPortraitMode?: boolean 124 isPortraitMode?: boolean
125
126 hlsPlaylist?: {
127 videoFilename: string
128 }
115} 129}
116 130
117function transcode (options: TranscodeOptions) { 131function transcode (options: TranscodeOptions) {
118 return new Promise<void>(async (res, rej) => { 132 return new Promise<void>(async (res, rej) => {
119 try { 133 try {
120 let fps = await getVideoFileFPS(options.inputPath)
121 // On small/medium resolutions, limit FPS
122 if (
123 options.resolution !== undefined &&
124 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
125 fps > VIDEO_TRANSCODING_FPS.AVERAGE
126 ) {
127 fps = VIDEO_TRANSCODING_FPS.AVERAGE
128 }
129
130 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) 134 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
131 .output(options.outputPath) 135 .output(options.outputPath)
132 command = await presetH264(command, options.resolution, fps) 136
137 if (options.hlsPlaylist) {
138 command = await buildHLSCommand(command, options)
139 } else {
140 command = await buildx264Command(command, options)
141 }
133 142
134 if (CONFIG.TRANSCODING.THREADS > 0) { 143 if (CONFIG.TRANSCODING.THREADS > 0) {
135 // if we don't set any threads ffmpeg will chose automatically 144 // if we don't set any threads ffmpeg will chose automatically
136 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) 145 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
137 } 146 }
138 147
139 if (options.resolution !== undefined) {
140 // '?x720' or '720x?' for example
141 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
142 command = command.size(size)
143 }
144
145 if (fps) {
146 // Hard FPS limits
147 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
148 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
149
150 command = command.withFPS(fps)
151 }
152
153 command 148 command
154 .on('error', (err, stdout, stderr) => { 149 .on('error', (err, stdout, stderr) => {
155 logger.error('Error in transcoding job.', { stdout, stderr }) 150 logger.error('Error in transcoding job.', { stdout, stderr })
156 return rej(err) 151 return rej(err)
157 }) 152 })
158 .on('end', res) 153 .on('end', () => {
154 return onTranscodingSuccess(options)
155 .then(() => res())
156 .catch(err => rej(err))
157 })
159 .run() 158 .run()
160 } catch (err) { 159 } catch (err) {
161 return rej(err) 160 return rej(err)
@@ -166,6 +165,7 @@ function transcode (options: TranscodeOptions) {
166// --------------------------------------------------------------------------- 165// ---------------------------------------------------------------------------
167 166
168export { 167export {
168 getVideoFileSize,
169 getVideoFileResolution, 169 getVideoFileResolution,
170 getDurationFromVideoFile, 170 getDurationFromVideoFile,
171 generateImageFromVideoFile, 171 generateImageFromVideoFile,
@@ -178,6 +178,71 @@ export {
178 178
179// --------------------------------------------------------------------------- 179// ---------------------------------------------------------------------------
180 180
181async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
182 let fps = await getVideoFileFPS(options.inputPath)
183 // On small/medium resolutions, limit FPS
184 if (
185 options.resolution !== undefined &&
186 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
187 fps > VIDEO_TRANSCODING_FPS.AVERAGE
188 ) {
189 fps = VIDEO_TRANSCODING_FPS.AVERAGE
190 }
191
192 command = await presetH264(command, options.resolution, fps)
193
194 if (options.resolution !== undefined) {
195 // '?x720' or '720x?' for example
196 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
197 command = command.size(size)
198 }
199
200 if (fps) {
201 // Hard FPS limits
202 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
203 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
204
205 command = command.withFPS(fps)
206 }
207
208 return command
209}
210
211async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
212 const videoPath = getHLSVideoPath(options)
213
214 command = await presetCopy(command)
215
216 command = command.outputOption('-hls_time 4')
217 .outputOption('-hls_list_size 0')
218 .outputOption('-hls_playlist_type vod')
219 .outputOption('-hls_segment_filename ' + videoPath)
220 .outputOption('-hls_segment_type fmp4')
221 .outputOption('-f hls')
222 .outputOption('-hls_flags single_file')
223
224 return command
225}
226
227function getHLSVideoPath (options: TranscodeOptions) {
228 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
229}
230
231async function onTranscodingSuccess (options: TranscodeOptions) {
232 if (!options.hlsPlaylist) return
233
234 // Fix wrong mapping with some ffmpeg versions
235 const fileContent = await readFile(options.outputPath)
236
237 const videoFileName = options.hlsPlaylist.videoFilename
238 const videoFilePath = getHLSVideoPath(options)
239
240 const newContent = fileContent.toString()
241 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
242
243 await writeFile(options.outputPath, newContent)
244}
245
181function getVideoFileStream (path: string) { 246function getVideoFileStream (path: string) {
182 return new Promise<any>((res, rej) => { 247 return new Promise<any>((res, rej) => {
183 ffmpeg.ffprobe(path, (err, metadata) => { 248 ffmpeg.ffprobe(path, (err, metadata) => {
@@ -348,3 +413,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
348 413
349 return localCommand 414 return localCommand
350} 415}
416
417async function presetCopy (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> {
418 return command
419 .format('mp4')
420 .videoCodec('copy')
421 .audioCodec('copy')
422}
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts
index e43ea3f1d..bd81aa3ba 100644
--- a/server/helpers/image-utils.ts
+++ b/server/helpers/image-utils.ts
@@ -4,18 +4,19 @@ import { readFile, remove } from 'fs-extra'
4import { logger } from './logger' 4import { logger } from './logger'
5 5
6async function processImage ( 6async function processImage (
7 physicalFile: { path: string }, 7 path: string,
8 destination: string, 8 destination: string,
9 newSize: { width: number, height: number } 9 newSize: { width: number, height: number },
10 keepOriginal = false
10) { 11) {
11 if (physicalFile.path === destination) { 12 if (path === destination) {
12 throw new Error('Sharp needs an input path different that the output path.') 13 throw new Error('Sharp needs an input path different that the output path.')
13 } 14 }
14 15
15 logger.debug('Processing image %s to %s.', physicalFile.path, destination) 16 logger.debug('Processing image %s to %s.', path, destination)
16 17
17 // Avoid sharp cache 18 // Avoid sharp cache
18 const buf = await readFile(physicalFile.path) 19 const buf = await readFile(path)
19 const sharpInstance = sharp(buf) 20 const sharpInstance = sharp(buf)
20 21
21 await remove(destination) 22 await remove(destination)
@@ -24,7 +25,7 @@ async function processImage (
24 .resize(newSize.width, newSize.height) 25 .resize(newSize.width, newSize.height)
25 .toFile(destination) 26 .toFile(destination)
26 27
27 await remove(physicalFile.path) 28 if (keepOriginal !== true) await remove(path)
28} 29}
29 30
30// --------------------------------------------------------------------------- 31// ---------------------------------------------------------------------------
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts
index 203e637a8..734523b01 100644
--- a/server/helpers/logger.ts
+++ b/server/helpers/logger.ts
@@ -2,11 +2,13 @@
2import { mkdirpSync } from 'fs-extra' 2import { mkdirpSync } from 'fs-extra'
3import * as path from 'path' 3import * as path from 'path'
4import * as winston from 'winston' 4import * as winston from 'winston'
5import { CONFIG } from '../initializers' 5import { CONFIG } from '../initializers/config'
6import { omit } from 'lodash'
6 7
7const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 8const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
8 9
9// Create the directory if it does not exist 10// Create the directory if it does not exist
11// FIXME: use async
10mkdirpSync(CONFIG.STORAGE.LOG_DIR) 12mkdirpSync(CONFIG.STORAGE.LOG_DIR)
11 13
12function loggerReplacer (key: string, value: any) { 14function loggerReplacer (key: string, value: any) {
@@ -22,13 +24,10 @@ function loggerReplacer (key: string, value: any) {
22} 24}
23 25
24const consoleLoggerFormat = winston.format.printf(info => { 26const consoleLoggerFormat = winston.format.printf(info => {
25 const obj = { 27 const obj = omit(info, 'label', 'timestamp', 'level', 'message')
26 meta: info.meta,
27 err: info.err,
28 sql: info.sql
29 }
30 28
31 let additionalInfos = JSON.stringify(obj, loggerReplacer, 2) 29 let additionalInfos = JSON.stringify(obj, loggerReplacer, 2)
30
32 if (additionalInfos === undefined || additionalInfos === '{}') additionalInfos = '' 31 if (additionalInfos === undefined || additionalInfos === '{}') additionalInfos = ''
33 else additionalInfos = ' ' + additionalInfos 32 else additionalInfos = ' ' + additionalInfos
34 33
@@ -57,7 +56,7 @@ const logger = winston.createLogger({
57 filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube.log'), 56 filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube.log'),
58 handleExceptions: true, 57 handleExceptions: true,
59 maxsize: 1024 * 1024 * 12, 58 maxsize: 1024 * 1024 * 12,
60 maxFiles: 5, 59 maxFiles: 20,
61 format: winston.format.combine( 60 format: winston.format.combine(
62 winston.format.timestamp(), 61 winston.format.timestamp(),
63 jsonLoggerFormat 62 jsonLoggerFormat
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts
index ab9ec077e..9148df2eb 100644
--- a/server/helpers/peertube-crypto.ts
+++ b/server/helpers/peertube-crypto.ts
@@ -1,5 +1,5 @@
1import { Request } from 'express' 1import { Request } from 'express'
2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers' 2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants'
3import { ActorModel } from '../models/activitypub/actor' 3import { ActorModel } from '../models/activitypub/actor'
4import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils' 4import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils'
5import { jsig, jsonld } from './custom-jsonld-signature' 5import { jsig, jsonld } from './custom-jsonld-signature'
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index 3fc776f1a..2e30c94a1 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -1,13 +1,16 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { createWriteStream } from 'fs-extra' 2import { createWriteStream, remove } from 'fs-extra'
3import * as request from 'request' 3import * as request from 'request'
4import { ACTIVITY_PUB, CONFIG } from '../initializers' 4import { ACTIVITY_PUB } from '../initializers/constants'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { join } from 'path' 6import { join } from 'path'
7import { logger } from './logger'
8import { CONFIG } from '../initializers/config'
7 9
8function doRequest <T> ( 10function doRequest <T> (
9 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } 11 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean },
10): Bluebird<{ response: request.RequestResponse, body: any }> { 12 bodyKBLimit = 1000 // 1MB
13): Bluebird<{ response: request.RequestResponse, body: T }> {
11 if (requestOptions.activityPub === true) { 14 if (requestOptions.activityPub === true) {
12 if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} 15 if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {}
13 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER 16 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
@@ -15,16 +18,29 @@ function doRequest <T> (
15 18
16 return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => { 19 return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => {
17 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) 20 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
21 .on('data', onRequestDataLengthCheck(bodyKBLimit))
18 }) 22 })
19} 23}
20 24
21function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.UriOptions, destPath: string) { 25function doRequestAndSaveToFile (
26 requestOptions: request.CoreOptions & request.UriOptions,
27 destPath: string,
28 bodyKBLimit = 10000 // 10MB
29) {
22 return new Bluebird<void>((res, rej) => { 30 return new Bluebird<void>((res, rej) => {
23 const file = createWriteStream(destPath) 31 const file = createWriteStream(destPath)
24 file.on('finish', () => res()) 32 file.on('finish', () => res())
25 33
26 request(requestOptions) 34 request(requestOptions)
27 .on('error', err => rej(err)) 35 .on('data', onRequestDataLengthCheck(bodyKBLimit))
36 .on('error', err => {
37 file.close()
38
39 remove(destPath)
40 .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err }))
41
42 return rej(err)
43 })
28 .pipe(file) 44 .pipe(file)
29 }) 45 })
30} 46}
@@ -34,7 +50,14 @@ async function downloadImage (url: string, destDir: string, destName: string, si
34 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) 50 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath)
35 51
36 const destPath = join(destDir, destName) 52 const destPath = join(destDir, destName)
37 await processImage({ path: tmpPath }, destPath, size) 53
54 try {
55 await processImage(tmpPath, destPath, size)
56 } catch (err) {
57 await remove(tmpPath)
58
59 throw err
60 }
38} 61}
39 62
40// --------------------------------------------------------------------------- 63// ---------------------------------------------------------------------------
@@ -44,3 +67,21 @@ export {
44 doRequestAndSaveToFile, 67 doRequestAndSaveToFile,
45 downloadImage 68 downloadImage
46} 69}
70
71// ---------------------------------------------------------------------------
72
73// Thanks to https://github.com/request/request/issues/2470#issuecomment-268929907 <3
74function onRequestDataLengthCheck (bodyKBLimit: number) {
75 let bufferLength = 0
76 const bytesLimit = bodyKBLimit * 1000
77
78 return function (chunk) {
79 bufferLength += chunk.length
80 if (bufferLength > bytesLimit) {
81 this.abort()
82
83 const error = new Error(`Response was too large - aborted after ${bytesLimit} bytes.`)
84 this.emit('error', error)
85 }
86 }
87}
diff --git a/server/helpers/signup.ts b/server/helpers/signup.ts
index cdce7989d..5eb56b3cf 100644
--- a/server/helpers/signup.ts
+++ b/server/helpers/signup.ts
@@ -1,6 +1,7 @@
1import { CONFIG } from '../initializers'
2import { UserModel } from '../models/account/user' 1import { UserModel } from '../models/account/user'
3import * as ipaddr from 'ipaddr.js' 2import * as ipaddr from 'ipaddr.js'
3import { CONFIG } from '../initializers/config'
4
4const isCidr = require('is-cidr') 5const isCidr = require('is-cidr')
5 6
6async function isSignupAllowed () { 7async function isSignupAllowed () {
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 3c3406e38..94ceb15e0 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -1,5 +1,4 @@
1import { ResultList } from '../../shared' 1import { ResultList } from '../../shared'
2import { CONFIG } from '../initializers'
3import { ApplicationModel } from '../models/application/application' 2import { ApplicationModel } from '../models/application/application'
4import { execPromise, execPromise2, pseudoRandomBytesPromise, sha256 } from './core-utils' 3import { execPromise, execPromise2, pseudoRandomBytesPromise, sha256 } from './core-utils'
5import { logger } from './logger' 4import { logger } from './logger'
@@ -7,7 +6,7 @@ import { join } from 'path'
7import { Instance as ParseTorrent } from 'parse-torrent' 6import { Instance as ParseTorrent } from 'parse-torrent'
8import { remove } from 'fs-extra' 7import { remove } from 'fs-extra'
9import * as memoizee from 'memoizee' 8import * as memoizee from 'memoizee'
10import { isArray } from './custom-validators/misc' 9import { CONFIG } from '../initializers/config'
11 10
12function deleteFileAsync (path: string) { 11function deleteFileAsync (path: string) {
13 remove(path) 12 remove(path)
diff --git a/server/helpers/video.ts b/server/helpers/video.ts
index 1bd21467d..c90fe06c7 100644
--- a/server/helpers/video.ts
+++ b/server/helpers/video.ts
@@ -1,10 +1,12 @@
1import { VideoModel } from '../models/video/video' 1import { VideoModel } from '../models/video/video'
2 2
3type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' 3type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'
4 4
5function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { 5function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) {
6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) 6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
7 7
8 if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
9
8 if (fetchType === 'only-video') return VideoModel.load(id) 10 if (fetchType === 'only-video') return VideoModel.load(id)
9 11
10 if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) 12 if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id)
diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts
index 156376943..049808846 100644
--- a/server/helpers/webfinger.ts
+++ b/server/helpers/webfinger.ts
@@ -3,7 +3,7 @@ import { WebFingerData } from '../../shared'
3import { ActorModel } from '../models/activitypub/actor' 3import { ActorModel } from '../models/activitypub/actor'
4import { isTestInstance } from './core-utils' 4import { isTestInstance } from './core-utils'
5import { isActivityPubUrlValid } from './custom-validators/activitypub/misc' 5import { isActivityPubUrlValid } from './custom-validators/activitypub/misc'
6import { CONFIG } from '../initializers' 6import { WEBSERVER } from '../initializers/constants'
7 7
8const webfinger = new WebFinger({ 8const webfinger = new WebFinger({
9 webfist_fallback: false, 9 webfist_fallback: false,
@@ -19,7 +19,7 @@ async function loadActorUrlOrGetFromWebfinger (uriArg: string) {
19 const [ name, host ] = uri.split('@') 19 const [ name, host ] = uri.split('@')
20 let actor: ActorModel 20 let actor: ActorModel
21 21
22 if (host === CONFIG.WEBSERVER.HOST) { 22 if (host === WEBSERVER.HOST) {
23 actor = await ActorModel.loadLocalByName(name) 23 actor = await ActorModel.loadLocalByName(name)
24 } else { 24 } else {
25 actor = await ActorModel.loadByNameAndHost(name, host) 25 actor = await ActorModel.loadByNameAndHost(name, host)
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts
index 3c9a0b96a..14dfe0d28 100644
--- a/server/helpers/webtorrent.ts
+++ b/server/helpers/webtorrent.ts
@@ -2,7 +2,7 @@ import { logger } from './logger'
2import { generateVideoImportTmpPath } from './utils' 2import { generateVideoImportTmpPath } from './utils'
3import * as WebTorrent from 'webtorrent' 3import * as WebTorrent from 'webtorrent'
4import { createWriteStream, ensureDir, remove } from 'fs-extra' 4import { createWriteStream, ensureDir, remove } from 'fs-extra'
5import { CONFIG } from '../initializers' 5import { CONFIG } from '../initializers/config'
6import { dirname, join } from 'path' 6import { dirname, join } from 'path'
7 7
8async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName?: string }, timeout: number) { 8async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName?: string }, timeout: number) {
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts
index b74351b42..b3079370f 100644
--- a/server/helpers/youtube-dl.ts
+++ b/server/helpers/youtube-dl.ts
@@ -1,5 +1,5 @@
1import { truncate } from 'lodash' 1import { truncate } from 'lodash'
2import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' 2import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers/constants'
3import { logger } from './logger' 3import { logger } from './logger'
4import { generateVideoImportTmpPath } from './utils' 4import { generateVideoImportTmpPath } from './utils'
5import { join } from 'path' 5import { join } from 'path'
@@ -16,6 +16,7 @@ export type YoutubeDLInfo = {
16 nsfw?: boolean 16 nsfw?: boolean
17 tags?: string[] 17 tags?: string[]
18 thumbnailUrl?: string 18 thumbnailUrl?: string
19 originallyPublishedAt?: Date
19} 20}
20 21
21const processOptions = { 22const processOptions = {
@@ -47,6 +48,11 @@ function downloadYoutubeDLVideo (url: string, timeout: number) {
47 48
48 const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] 49 const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
49 50
51 if (process.env.FFMPEG_PATH) {
52 options.push('--ffmpeg-location')
53 options.push(process.env.FFMPEG_PATH)
54 }
55
50 return new Promise<string>(async (res, rej) => { 56 return new Promise<string>(async (res, rej) => {
51 const youtubeDL = await safeGetYoutubeDL() 57 const youtubeDL = await safeGetYoutubeDL()
52 youtubeDL.exec(url, options, processOptions, err => { 58 youtubeDL.exec(url, options, processOptions, err => {
@@ -142,13 +148,33 @@ async function safeGetYoutubeDL () {
142 return youtubeDL 148 return youtubeDL
143} 149}
144 150
151function buildOriginallyPublishedAt (obj: any) {
152 let originallyPublishedAt: Date = null
153
154 const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date)
155 if (uploadDateMatcher) {
156 originallyPublishedAt = new Date()
157 originallyPublishedAt.setHours(0, 0, 0, 0)
158
159 const year = parseInt(uploadDateMatcher[1], 10)
160 // Month starts from 0
161 const month = parseInt(uploadDateMatcher[2], 10) - 1
162 const day = parseInt(uploadDateMatcher[3], 10)
163
164 originallyPublishedAt.setFullYear(year, month, day)
165 }
166
167 return originallyPublishedAt
168}
169
145// --------------------------------------------------------------------------- 170// ---------------------------------------------------------------------------
146 171
147export { 172export {
148 updateYoutubeDLBinary, 173 updateYoutubeDLBinary,
149 downloadYoutubeDLVideo, 174 downloadYoutubeDLVideo,
150 getYoutubeDLInfo, 175 getYoutubeDLInfo,
151 safeGetYoutubeDL 176 safeGetYoutubeDL,
177 buildOriginallyPublishedAt
152} 178}
153 179
154// --------------------------------------------------------------------------- 180// ---------------------------------------------------------------------------
@@ -180,7 +206,8 @@ function buildVideoInfo (obj: any) {
180 licence: getLicence(obj.license), 206 licence: getLicence(obj.license),
181 nsfw: isNSFW(obj), 207 nsfw: isNSFW(obj),
182 tags: getTags(obj.tags), 208 tags: getTags(obj.tags),
183 thumbnailUrl: obj.thumbnail || undefined 209 thumbnailUrl: obj.thumbnail || undefined,
210 originallyPublishedAt: buildOriginallyPublishedAt(obj)
184 } 211 }
185} 212}
186 213