aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers
diff options
context:
space:
mode:
Diffstat (limited to 'server/controllers')
-rw-r--r--server/controllers/api/config.ts12
-rw-r--r--server/controllers/api/jobs.ts4
-rw-r--r--server/controllers/api/plugins.ts1
-rw-r--r--server/controllers/api/search.ts43
-rw-r--r--server/controllers/api/users/index.ts8
-rw-r--r--server/controllers/api/users/me.ts10
-rw-r--r--server/controllers/api/users/my-notifications.ts4
-rw-r--r--server/controllers/api/users/my-subscriptions.ts12
-rw-r--r--server/controllers/api/users/token.ts72
-rw-r--r--server/controllers/api/video-channel.ts66
-rw-r--r--server/controllers/api/videos/index.ts11
-rw-r--r--server/controllers/api/videos/ownership.ts2
-rw-r--r--server/controllers/client.ts28
-rw-r--r--server/controllers/download.ts85
-rw-r--r--server/controllers/feeds.ts10
-rw-r--r--server/controllers/lazy-static.ts41
-rw-r--r--server/controllers/plugins.ts14
-rw-r--r--server/controllers/services.ts4
-rw-r--r--server/controllers/static.ts4
19 files changed, 322 insertions, 109 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index fb108ca1c..e28f7502d 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -158,9 +158,17 @@ async function getConfig (req: express.Request, res: express.Response) {
158 avatar: { 158 avatar: {
159 file: { 159 file: {
160 size: { 160 size: {
161 max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max 161 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
162 }, 162 },
163 extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME 163 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
164 }
165 },
166 banner: {
167 file: {
168 size: {
169 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
170 },
171 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
164 } 172 }
165 }, 173 },
166 video: { 174 video: {
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts
index 861cc22b9..d7cee1605 100644
--- a/server/controllers/api/jobs.ts
+++ b/server/controllers/api/jobs.ts
@@ -9,10 +9,10 @@ import {
9 authenticate, 9 authenticate,
10 ensureUserHasRight, 10 ensureUserHasRight,
11 jobsSortValidator, 11 jobsSortValidator,
12 paginationValidatorBuilder,
12 setDefaultPagination, 13 setDefaultPagination,
13 setDefaultSort 14 setDefaultSort
14} from '../../middlewares' 15} from '../../middlewares'
15import { paginationValidator } from '../../middlewares/validators'
16import { listJobsValidator } from '../../middlewares/validators/jobs' 16import { listJobsValidator } from '../../middlewares/validators/jobs'
17 17
18const jobsRouter = express.Router() 18const jobsRouter = express.Router()
@@ -20,7 +20,7 @@ const jobsRouter = express.Router()
20jobsRouter.get('/:state?', 20jobsRouter.get('/:state?',
21 authenticate, 21 authenticate,
22 ensureUserHasRight(UserRight.MANAGE_JOBS), 22 ensureUserHasRight(UserRight.MANAGE_JOBS),
23 paginationValidator, 23 paginationValidatorBuilder([ 'jobs' ]),
24 jobsSortValidator, 24 jobsSortValidator,
25 setDefaultSort, 25 setDefaultSort,
26 setDefaultPagination, 26 setDefaultPagination,
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts
index 1c0b5edb1..bb69f25a1 100644
--- a/server/controllers/api/plugins.ts
+++ b/server/controllers/api/plugins.ts
@@ -205,7 +205,6 @@ async function listAvailablePlugins (req: express.Request, res: express.Response
205 if (!resultList) { 205 if (!resultList) {
206 return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503) 206 return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503)
207 .json({ error: 'Plugin index unavailable. Please retry later' }) 207 .json({ error: 'Plugin index unavailable. Please retry later' })
208 .end()
209 } 208 }
210 209
211 return res.json(resultList) 210 return res.json(resultList)
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index 7e1b7b230..f0cdf3a89 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -1,8 +1,9 @@
1import * as express from 'express' 1import * as express from 'express'
2import { sanitizeUrl } from '@server/helpers/core-utils' 2import { sanitizeUrl } from '@server/helpers/core-utils'
3import { doRequest } from '@server/helpers/requests' 3import { doJSONRequest } from '@server/helpers/requests'
4import { CONFIG } from '@server/initializers/config' 4import { CONFIG } from '@server/initializers/config'
5import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos' 5import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos'
6import { Hooks } from '@server/lib/plugins/hooks'
6import { AccountBlocklistModel } from '@server/models/account/account-blocklist' 7import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
7import { getServerActor } from '@server/models/application/application' 8import { getServerActor } from '@server/models/application/application'
8import { ServerBlocklistModel } from '@server/models/server/server-blocklist' 9import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
@@ -22,8 +23,8 @@ import {
22 paginationValidator, 23 paginationValidator,
23 setDefaultPagination, 24 setDefaultPagination,
24 setDefaultSearchSort, 25 setDefaultSearchSort,
25 videoChannelsSearchSortValidator,
26 videoChannelsListSearchValidator, 26 videoChannelsListSearchValidator,
27 videoChannelsSearchSortValidator,
27 videosSearchSortValidator, 28 videosSearchSortValidator,
28 videosSearchValidator 29 videosSearchValidator
29} from '../../middlewares' 30} from '../../middlewares'
@@ -87,16 +88,17 @@ function searchVideoChannels (req: express.Request, res: express.Response) {
87async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { 88async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) {
88 const result = await buildMutedForSearchIndex(res) 89 const result = await buildMutedForSearchIndex(res)
89 90
90 const body = Object.assign(query, result) 91 const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params')
91 92
92 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' 93 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels'
93 94
94 try { 95 try {
95 logger.debug('Doing video channels search index request on %s.', url, { body }) 96 logger.debug('Doing video channels search index request on %s.', url, { body })
96 97
97 const searchIndexResult = await doRequest<ResultList<VideoChannel>>({ uri: url, body, json: true }) 98 const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body })
99 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result')
98 100
99 return res.json(searchIndexResult.body) 101 return res.json(jsonResult)
100 } catch (err) { 102 } catch (err) {
101 logger.warn('Cannot use search index to make video channels search.', { err }) 103 logger.warn('Cannot use search index to make video channels search.', { err })
102 104
@@ -107,14 +109,19 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: e
107async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { 109async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) {
108 const serverActor = await getServerActor() 110 const serverActor = await getServerActor()
109 111
110 const options = { 112 const apiOptions = await Hooks.wrapObject({
111 actorId: serverActor.id, 113 actorId: serverActor.id,
112 search: query.search, 114 search: query.search,
113 start: query.start, 115 start: query.start,
114 count: query.count, 116 count: query.count,
115 sort: query.sort 117 sort: query.sort
116 } 118 }, 'filter:api.search.video-channels.local.list.params')
117 const resultList = await VideoChannelModel.searchForApi(options) 119
120 const resultList = await Hooks.wrapPromiseFun(
121 VideoChannelModel.searchForApi,
122 apiOptions,
123 'filter:api.search.video-channels.local.list.result'
124 )
118 125
119 return res.json(getFormattedObjects(resultList.data, resultList.total)) 126 return res.json(getFormattedObjects(resultList.data, resultList.total))
120} 127}
@@ -168,7 +175,7 @@ function searchVideos (req: express.Request, res: express.Response) {
168async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { 175async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) {
169 const result = await buildMutedForSearchIndex(res) 176 const result = await buildMutedForSearchIndex(res)
170 177
171 const body: VideosSearchQuery = Object.assign(query, result) 178 let body: VideosSearchQuery = Object.assign(query, result)
172 179
173 // Use the default instance NSFW policy if not specified 180 // Use the default instance NSFW policy if not specified
174 if (!body.nsfw) { 181 if (!body.nsfw) {
@@ -181,14 +188,17 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
181 : 'both' 188 : 'both'
182 } 189 }
183 190
191 body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params')
192
184 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' 193 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos'
185 194
186 try { 195 try {
187 logger.debug('Doing videos search index request on %s.', url, { body }) 196 logger.debug('Doing videos search index request on %s.', url, { body })
188 197
189 const searchIndexResult = await doRequest<ResultList<Video>>({ uri: url, body, json: true }) 198 const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body })
199 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result')
190 200
191 return res.json(searchIndexResult.body) 201 return res.json(jsonResult)
192 } catch (err) { 202 } catch (err) {
193 logger.warn('Cannot use search index to make video search.', { err }) 203 logger.warn('Cannot use search index to make video search.', { err })
194 204
@@ -197,13 +207,18 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
197} 207}
198 208
199async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { 209async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
200 const options = Object.assign(query, { 210 const apiOptions = await Hooks.wrapObject(Object.assign(query, {
201 includeLocalVideos: true, 211 includeLocalVideos: true,
202 nsfw: buildNSFWFilter(res, query.nsfw), 212 nsfw: buildNSFWFilter(res, query.nsfw),
203 filter: query.filter, 213 filter: query.filter,
204 user: res.locals.oauth ? res.locals.oauth.token.User : undefined 214 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
205 }) 215 }), 'filter:api.search.videos.local.list.params')
206 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) 216
217 const resultList = await Hooks.wrapPromiseFun(
218 VideoModel.searchAndPopulateAccountAndServer,
219 apiOptions,
220 'filter:api.search.videos.local.list.result'
221 )
207 222
208 return res.json(getFormattedObjects(resultList.data, resultList.total)) 223 return res.json(getFormattedObjects(resultList.data, resultList.total))
209} 224}
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 3be1d55ae..e2b1ea7cd 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -2,8 +2,10 @@ import * as express from 'express'
2import * as RateLimit from 'express-rate-limit' 2import * as RateLimit from 'express-rate-limit'
3import { tokensRouter } from '@server/controllers/api/users/token' 3import { tokensRouter } from '@server/controllers/api/users/token'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
5import { MUser, MUserAccountDefault } from '@server/types/models' 6import { MUser, MUserAccountDefault } from '@server/types/models'
6import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' 7import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
8import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 9import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
8import { UserRegister } from '../../../../shared/models/users/user-register.model' 10import { UserRegister } from '../../../../shared/models/users/user-register.model'
9import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 11import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
@@ -14,7 +16,6 @@ import { WEBSERVER } from '../../../initializers/constants'
14import { sequelizeTypescript } from '../../../initializers/database' 16import { sequelizeTypescript } from '../../../initializers/database'
15import { Emailer } from '../../../lib/emailer' 17import { Emailer } from '../../../lib/emailer'
16import { Notifier } from '../../../lib/notifier' 18import { Notifier } from '../../../lib/notifier'
17import { deleteUserToken } from '../../../lib/oauth-model'
18import { Redis } from '../../../lib/redis' 19import { Redis } from '../../../lib/redis'
19import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' 20import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
20import { 21import {
@@ -52,7 +53,6 @@ import { myVideosHistoryRouter } from './my-history'
52import { myNotificationsRouter } from './my-notifications' 53import { myNotificationsRouter } from './my-notifications'
53import { mySubscriptionsRouter } from './my-subscriptions' 54import { mySubscriptionsRouter } from './my-subscriptions'
54import { myVideoPlaylistsRouter } from './my-video-playlists' 55import { myVideoPlaylistsRouter } from './my-video-playlists'
55import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
56 56
57const auditLogger = auditLoggerFactory('users') 57const auditLogger = auditLoggerFactory('users')
58 58
@@ -335,7 +335,7 @@ async function updateUser (req: express.Request, res: express.Response) {
335 const user = await userToUpdate.save() 335 const user = await userToUpdate.save()
336 336
337 // Destroy user token to refresh rights 337 // Destroy user token to refresh rights
338 if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id) 338 if (roleChanged || body.password !== undefined) await OAuthTokenModel.deleteUserToken(userToUpdate.id)
339 339
340 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) 340 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
341 341
@@ -395,7 +395,7 @@ async function changeUserBlock (res: express.Response, user: MUserAccountDefault
395 user.blockedReason = reason || null 395 user.blockedReason = reason || null
396 396
397 await sequelizeTypescript.transaction(async t => { 397 await sequelizeTypescript.transaction(async t => {
398 await deleteUserToken(user.id, t) 398 await OAuthTokenModel.deleteUserToken(user.id, t)
399 399
400 await user.save({ transaction: t }) 400 await user.save({ transaction: t })
401 }) 401 })
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 5a3e9e51a..9f9d2d77f 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -2,7 +2,7 @@ import 'multer'
2import * as express from 'express' 2import * as express from 'express'
3import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' 3import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' 5import { ActorImageType, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared'
6import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' 7import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
8import { createReqFiles } from '../../../helpers/express-utils' 8import { createReqFiles } from '../../../helpers/express-utils'
@@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config'
11import { MIMETYPES } from '../../../initializers/constants' 11import { MIMETYPES } from '../../../initializers/constants'
12import { sequelizeTypescript } from '../../../initializers/database' 12import { sequelizeTypescript } from '../../../initializers/database'
13import { sendUpdateActor } from '../../../lib/activitypub/send' 13import { sendUpdateActor } from '../../../lib/activitypub/send'
14import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/avatar' 14import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/actor-image'
15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' 15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
16import { 16import {
17 asyncMiddleware, 17 asyncMiddleware,
@@ -25,7 +25,7 @@ import {
25 usersVideoRatingValidator 25 usersVideoRatingValidator
26} from '../../../middlewares' 26} from '../../../middlewares'
27import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' 27import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators'
28import { updateAvatarValidator } from '../../../middlewares/validators/avatar' 28import { updateAvatarValidator } from '../../../middlewares/validators/actor-image'
29import { AccountModel } from '../../../models/account/account' 29import { AccountModel } from '../../../models/account/account'
30import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 30import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
31import { UserModel } from '../../../models/account/user' 31import { UserModel } from '../../../models/account/user'
@@ -238,7 +238,7 @@ async function updateMyAvatar (req: express.Request, res: express.Response) {
238 238
239 const userAccount = await AccountModel.load(user.Account.id) 239 const userAccount = await AccountModel.load(user.Account.id)
240 240
241 const avatar = await updateLocalActorAvatarFile(userAccount, avatarPhysicalFile) 241 const avatar = await updateLocalActorImageFile(userAccount, avatarPhysicalFile, ActorImageType.AVATAR)
242 242
243 return res.json({ avatar: avatar.toFormattedJSON() }) 243 return res.json({ avatar: avatar.toFormattedJSON() })
244} 244}
@@ -247,7 +247,7 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) {
247 const user = res.locals.oauth.token.user 247 const user = res.locals.oauth.token.user
248 248
249 const userAccount = await AccountModel.load(user.Account.id) 249 const userAccount = await AccountModel.load(user.Account.id)
250 await deleteLocalActorAvatarFile(userAccount) 250 await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR)
251 251
252 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 252 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
253} 253}
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts
index 5f5e4c5e6..0a9101a46 100644
--- a/server/controllers/api/users/my-notifications.ts
+++ b/server/controllers/api/users/my-notifications.ts
@@ -80,7 +80,9 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
80 newInstanceFollower: body.newInstanceFollower, 80 newInstanceFollower: body.newInstanceFollower,
81 autoInstanceFollowing: body.autoInstanceFollowing, 81 autoInstanceFollowing: body.autoInstanceFollowing,
82 abuseNewMessage: body.abuseNewMessage, 82 abuseNewMessage: body.abuseNewMessage,
83 abuseStateChange: body.abuseStateChange 83 abuseStateChange: body.abuseStateChange,
84 newPeerTubeVersion: body.newPeerTubeVersion,
85 newPluginVersion: body.newPluginVersion
84 } 86 }
85 87
86 await UserNotificationSettingModel.update(values, query) 88 await UserNotificationSettingModel.update(values, query)
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index ec77ddd7a..e8949ee59 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -1,5 +1,8 @@
1import 'multer' 1import 'multer'
2import * as express from 'express' 2import * as express from 'express'
3import { sendUndoFollow } from '@server/lib/activitypub/send'
4import { VideoChannelModel } from '@server/models/video/video-channel'
5import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
3import { VideoFilter } from '../../../../shared/models/videos/video-query.type' 6import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
4import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' 7import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
5import { getFormattedObjects } from '../../../helpers/utils' 8import { getFormattedObjects } from '../../../helpers/utils'
@@ -26,8 +29,6 @@ import {
26} from '../../../middlewares/validators' 29} from '../../../middlewares/validators'
27import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 30import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
28import { VideoModel } from '../../../models/video/video' 31import { VideoModel } from '../../../models/video/video'
29import { sendUndoFollow } from '@server/lib/activitypub/send'
30import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
31 32
32const mySubscriptionsRouter = express.Router() 33const mySubscriptionsRouter = express.Router()
33 34
@@ -66,7 +67,7 @@ mySubscriptionsRouter.post('/me/subscriptions',
66mySubscriptionsRouter.get('/me/subscriptions/:uri', 67mySubscriptionsRouter.get('/me/subscriptions/:uri',
67 authenticate, 68 authenticate,
68 userSubscriptionGetValidator, 69 userSubscriptionGetValidator,
69 getUserSubscription 70 asyncMiddleware(getUserSubscription)
70) 71)
71 72
72mySubscriptionsRouter.delete('/me/subscriptions/:uri', 73mySubscriptionsRouter.delete('/me/subscriptions/:uri',
@@ -130,10 +131,11 @@ function addUserSubscription (req: express.Request, res: express.Response) {
130 return res.status(HttpStatusCode.NO_CONTENT_204).end() 131 return res.status(HttpStatusCode.NO_CONTENT_204).end()
131} 132}
132 133
133function getUserSubscription (req: express.Request, res: express.Response) { 134async function getUserSubscription (req: express.Request, res: express.Response) {
134 const subscription = res.locals.subscription 135 const subscription = res.locals.subscription
136 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(subscription.ActorFollowing.VideoChannel.id)
135 137
136 return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON()) 138 return res.json(videoChannel.toFormattedJSON())
137} 139}
138 140
139async function deleteUserSubscription (req: express.Request, res: express.Response) { 141async function deleteUserSubscription (req: express.Request, res: express.Response) {
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts
index 821429358..694bb0a92 100644
--- a/server/controllers/api/users/token.ts
+++ b/server/controllers/api/users/token.ts
@@ -1,11 +1,14 @@
1import { handleLogin, handleTokenRevocation } from '@server/lib/auth' 1import * as express from 'express'
2import * as RateLimit from 'express-rate-limit' 2import * as RateLimit from 'express-rate-limit'
3import { v4 as uuidv4 } from 'uuid'
4import { logger } from '@server/helpers/logger'
3import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
4import * as express from 'express' 6import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
7import { handleOAuthToken } from '@server/lib/auth/oauth'
8import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
5import { Hooks } from '@server/lib/plugins/hooks' 9import { Hooks } from '@server/lib/plugins/hooks'
6import { asyncMiddleware, authenticate } from '@server/middlewares' 10import { asyncMiddleware, authenticate } from '@server/middlewares'
7import { ScopedToken } from '@shared/models/users/user-scoped-token' 11import { ScopedToken } from '@shared/models/users/user-scoped-token'
8import { v4 as uuidv4 } from 'uuid'
9 12
10const tokensRouter = express.Router() 13const tokensRouter = express.Router()
11 14
@@ -16,8 +19,7 @@ const loginRateLimiter = RateLimit({
16 19
17tokensRouter.post('/token', 20tokensRouter.post('/token',
18 loginRateLimiter, 21 loginRateLimiter,
19 handleLogin, 22 asyncMiddleware(handleToken)
20 tokenSuccess
21) 23)
22 24
23tokensRouter.post('/revoke-token', 25tokensRouter.post('/revoke-token',
@@ -42,10 +44,53 @@ export {
42} 44}
43// --------------------------------------------------------------------------- 45// ---------------------------------------------------------------------------
44 46
45function tokenSuccess (req: express.Request) { 47async function handleToken (req: express.Request, res: express.Response, next: express.NextFunction) {
46 const username = req.body.username 48 const grantType = req.body.grant_type
49
50 try {
51 const bypassLogin = await buildByPassLogin(req, grantType)
52
53 const refreshTokenAuthName = grantType === 'refresh_token'
54 ? await getAuthNameFromRefreshGrant(req.body.refresh_token)
55 : undefined
56
57 const options = {
58 refreshTokenAuthName,
59 bypassLogin
60 }
61
62 const token = await handleOAuthToken(req, options)
63
64 res.set('Cache-Control', 'no-store')
65 res.set('Pragma', 'no-cache')
66
67 Hooks.runAction('action:api.user.oauth2-got-token', { username: token.user.username, ip: req.ip })
68
69 return res.json({
70 token_type: 'Bearer',
47 71
48 Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip }) 72 access_token: token.accessToken,
73 refresh_token: token.refreshToken,
74
75 expires_in: token.accessTokenExpiresIn,
76 refresh_token_expires_in: token.refreshTokenExpiresIn
77 })
78 } catch (err) {
79 logger.warn('Login error', { err })
80
81 return res.status(err.code || 400).json({
82 code: err.name,
83 error: err.message
84 })
85 }
86}
87
88async function handleTokenRevocation (req: express.Request, res: express.Response) {
89 const token = res.locals.oauth.token
90
91 const result = await revokeToken(token, { req, explicitLogout: true })
92
93 return res.json(result)
49} 94}
50 95
51function getScopedTokens (req: express.Request, res: express.Response) { 96function getScopedTokens (req: express.Request, res: express.Response) {
@@ -66,3 +111,14 @@ async function renewScopedTokens (req: express.Request, res: express.Response) {
66 feedToken: user.feedToken 111 feedToken: user.feedToken
67 } as ScopedToken) 112 } as ScopedToken)
68} 113}
114
115async function buildByPassLogin (req: express.Request, grantType: string): Promise<BypassLogin> {
116 if (grantType !== 'password') return undefined
117
118 if (req.body.externalAuthToken) {
119 // Consistency with the getBypassFromPasswordGrant promise
120 return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
121 }
122
123 return getBypassFromPasswordGrant(req.body.username, req.body.password)
124}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 03617dc8d..149d6cfb4 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -1,8 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import { Hooks } from '@server/lib/plugins/hooks' 2import { Hooks } from '@server/lib/plugins/hooks'
3import { getServerActor } from '@server/models/application/application' 3import { getServerActor } from '@server/models/application/application'
4import { MChannelAccountDefault } from '@server/types/models' 4import { MChannelBannerAccountDefault } from '@server/types/models'
5import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 5import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' 7import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
8import { resetSequelizeInstance } from '../../helpers/database-utils' 8import { resetSequelizeInstance } from '../../helpers/database-utils'
@@ -13,7 +13,7 @@ import { CONFIG } from '../../initializers/config'
13import { MIMETYPES } from '../../initializers/constants' 13import { MIMETYPES } from '../../initializers/constants'
14import { sequelizeTypescript } from '../../initializers/database' 14import { sequelizeTypescript } from '../../initializers/database'
15import { sendUpdateActor } from '../../lib/activitypub/send' 15import { sendUpdateActor } from '../../lib/activitypub/send'
16import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/avatar' 16import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/actor-image'
17import { JobQueue } from '../../lib/job-queue' 17import { JobQueue } from '../../lib/job-queue'
18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' 18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
19import { 19import {
@@ -33,7 +33,7 @@ import {
33 videoPlaylistsSortValidator 33 videoPlaylistsSortValidator
34} from '../../middlewares' 34} from '../../middlewares'
35import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' 35import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators'
36import { updateAvatarValidator } from '../../middlewares/validators/avatar' 36import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image'
37import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' 37import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
38import { AccountModel } from '../../models/account/account' 38import { AccountModel } from '../../models/account/account'
39import { VideoModel } from '../../models/video/video' 39import { VideoModel } from '../../models/video/video'
@@ -42,6 +42,7 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist'
42 42
43const auditLogger = auditLoggerFactory('channels') 43const auditLogger = auditLoggerFactory('channels')
44const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) 44const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
45const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { bannerfile: CONFIG.STORAGE.TMP_DIR })
45 46
46const videoChannelRouter = express.Router() 47const videoChannelRouter = express.Router()
47 48
@@ -69,6 +70,15 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
69 asyncMiddleware(updateVideoChannelAvatar) 70 asyncMiddleware(updateVideoChannelAvatar)
70) 71)
71 72
73videoChannelRouter.post('/:nameWithHost/banner/pick',
74 authenticate,
75 reqBannerFile,
76 // Check the rights
77 asyncMiddleware(videoChannelsUpdateValidator),
78 updateBannerValidator,
79 asyncMiddleware(updateVideoChannelBanner)
80)
81
72videoChannelRouter.delete('/:nameWithHost/avatar', 82videoChannelRouter.delete('/:nameWithHost/avatar',
73 authenticate, 83 authenticate,
74 // Check the rights 84 // Check the rights
@@ -76,6 +86,13 @@ videoChannelRouter.delete('/:nameWithHost/avatar',
76 asyncMiddleware(deleteVideoChannelAvatar) 86 asyncMiddleware(deleteVideoChannelAvatar)
77) 87)
78 88
89videoChannelRouter.delete('/:nameWithHost/banner',
90 authenticate,
91 // Check the rights
92 asyncMiddleware(videoChannelsUpdateValidator),
93 asyncMiddleware(deleteVideoChannelBanner)
94)
95
79videoChannelRouter.put('/:nameWithHost', 96videoChannelRouter.put('/:nameWithHost',
80 authenticate, 97 authenticate,
81 asyncMiddleware(videoChannelsUpdateValidator), 98 asyncMiddleware(videoChannelsUpdateValidator),
@@ -134,26 +151,41 @@ async function listVideoChannels (req: express.Request, res: express.Response) {
134 return res.json(getFormattedObjects(resultList.data, resultList.total)) 151 return res.json(getFormattedObjects(resultList.data, resultList.total))
135} 152}
136 153
154async function updateVideoChannelBanner (req: express.Request, res: express.Response) {
155 const bannerPhysicalFile = req.files['bannerfile'][0]
156 const videoChannel = res.locals.videoChannel
157 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
158
159 const banner = await updateLocalActorImageFile(videoChannel, bannerPhysicalFile, ActorImageType.BANNER)
160
161 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
162
163 return res.json({ banner: banner.toFormattedJSON() })
164}
137async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { 165async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
138 const avatarPhysicalFile = req.files['avatarfile'][0] 166 const avatarPhysicalFile = req.files['avatarfile'][0]
139 const videoChannel = res.locals.videoChannel 167 const videoChannel = res.locals.videoChannel
140 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) 168 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
141 169
142 const avatar = await updateLocalActorAvatarFile(videoChannel, avatarPhysicalFile) 170 const avatar = await updateLocalActorImageFile(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR)
143 171
144 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) 172 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
145 173
146 return res 174 return res.json({ avatar: avatar.toFormattedJSON() })
147 .json({
148 avatar: avatar.toFormattedJSON()
149 })
150 .end()
151} 175}
152 176
153async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { 177async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
154 const videoChannel = res.locals.videoChannel 178 const videoChannel = res.locals.videoChannel
155 179
156 await deleteLocalActorAvatarFile(videoChannel) 180 await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR)
181
182 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
183}
184
185async function deleteVideoChannelBanner (req: express.Request, res: express.Response) {
186 const videoChannel = res.locals.videoChannel
187
188 await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER)
157 189
158 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 190 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
159} 191}
@@ -177,7 +209,7 @@ async function addVideoChannel (req: express.Request, res: express.Response) {
177 videoChannel: { 209 videoChannel: {
178 id: videoChannelCreated.id 210 id: videoChannelCreated.id
179 } 211 }
180 }).end() 212 })
181} 213}
182 214
183async function updateVideoChannel (req: express.Request, res: express.Response) { 215async function updateVideoChannel (req: express.Request, res: express.Response) {
@@ -206,7 +238,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
206 } 238 }
207 } 239 }
208 240
209 const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelAccountDefault 241 const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelBannerAccountDefault
210 await sendUpdateActor(videoChannelInstanceUpdated, t) 242 await sendUpdateActor(videoChannelInstanceUpdated, t)
211 243
212 auditLogger.update( 244 auditLogger.update(
@@ -252,13 +284,13 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
252} 284}
253 285
254async function getVideoChannel (req: express.Request, res: express.Response) { 286async function getVideoChannel (req: express.Request, res: express.Response) {
255 const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) 287 const videoChannel = res.locals.videoChannel
256 288
257 if (videoChannelWithVideos.isOutdated()) { 289 if (videoChannel.isOutdated()) {
258 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } }) 290 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannel.Actor.url } })
259 } 291 }
260 292
261 return res.json(videoChannelWithVideos.toFormattedJSON()) 293 return res.json(videoChannel.toFormattedJSON())
262} 294}
263 295
264async function listVideoChannelPlaylists (req: express.Request, res: express.Response) { 296async function listVideoChannelPlaylists (req: express.Request, res: express.Response) {
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 2447c1288..7fee278f2 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -17,7 +17,7 @@ import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../
17import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' 17import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
18import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' 18import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
19import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' 19import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
20import { logger } from '../../../helpers/logger' 20import { logger, loggerTagsFactory } from '../../../helpers/logger'
21import { getFormattedObjects } from '../../../helpers/utils' 21import { getFormattedObjects } from '../../../helpers/utils'
22import { CONFIG } from '../../../initializers/config' 22import { CONFIG } from '../../../initializers/config'
23import { 23import {
@@ -67,6 +67,7 @@ import { ownershipVideoRouter } from './ownership'
67import { rateVideoRouter } from './rate' 67import { rateVideoRouter } from './rate'
68import { watchingRouter } from './watching' 68import { watchingRouter } from './watching'
69 69
70const lTags = loggerTagsFactory('api', 'video')
70const auditLogger = auditLoggerFactory('videos') 71const auditLogger = auditLoggerFactory('videos')
71const videosRouter = express.Router() 72const videosRouter = express.Router()
72 73
@@ -257,14 +258,14 @@ async function addVideo (req: express.Request, res: express.Response) {
257 }) 258 })
258 259
259 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) 260 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
260 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) 261 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
261 262
262 return { videoCreated } 263 return { videoCreated }
263 }) 264 })
264 265
265 // Create the torrent file in async way because it could be long 266 // Create the torrent file in async way because it could be long
266 createTorrentAndSetInfoHashAsync(video, videoFile) 267 createTorrentAndSetInfoHashAsync(video, videoFile)
267 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err })) 268 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
268 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) 269 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
269 .then(refreshedVideo => { 270 .then(refreshedVideo => {
270 if (!refreshedVideo) return 271 if (!refreshedVideo) return
@@ -276,7 +277,7 @@ async function addVideo (req: express.Request, res: express.Response) {
276 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t)) 277 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
277 }) 278 })
278 }) 279 })
279 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err })) 280 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
280 281
281 if (video.state === VideoState.TO_TRANSCODE) { 282 if (video.state === VideoState.TO_TRANSCODE) {
282 await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User) 283 await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User)
@@ -389,7 +390,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
389 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), 390 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
390 oldVideoAuditView 391 oldVideoAuditView
391 ) 392 )
392 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) 393 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid))
393 394
394 return videoInstanceUpdated 395 return videoInstanceUpdated
395 }) 396 })
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts
index 86adb6c69..a85d7c30b 100644
--- a/server/controllers/api/videos/ownership.ts
+++ b/server/controllers/api/videos/ownership.ts
@@ -107,7 +107,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
107 // We need more attributes for federation 107 // We need more attributes for federation
108 const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id) 108 const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id)
109 109
110 const oldVideoChannel = await VideoChannelModel.loadByIdAndPopulateAccount(targetVideo.channelId) 110 const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId)
111 111
112 targetVideo.channelId = channel.id 112 targetVideo.channelId = channel.id
113 113
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index 557cbfdfb..022a17ff4 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -2,7 +2,9 @@ import * as express from 'express'
2import { constants, promises as fs } from 'fs' 2import { constants, promises as fs } from 'fs'
3import { readFile } from 'fs-extra' 3import { readFile } from 'fs-extra'
4import { join } from 'path' 4import { join } from 'path'
5import { logger } from '@server/helpers/logger'
5import { CONFIG } from '@server/initializers/config' 6import { CONFIG } from '@server/initializers/config'
7import { Hooks } from '@server/lib/plugins/hooks'
6import { HttpStatusCode } from '@shared/core-utils' 8import { HttpStatusCode } from '@shared/core-utils'
7import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n' 9import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n'
8import { root } from '../helpers/core-utils' 10import { root } from '../helpers/core-utils'
@@ -27,6 +29,7 @@ const embedMiddlewares = [
27 ? embedCSP 29 ? embedCSP
28 : (req: express.Request, res: express.Response, next: express.NextFunction) => next(), 30 : (req: express.Request, res: express.Response, next: express.NextFunction) => next(),
29 31
32 // Set headers
30 (req: express.Request, res: express.Response, next: express.NextFunction) => { 33 (req: express.Request, res: express.Response, next: express.NextFunction) => {
31 res.removeHeader('X-Frame-Options') 34 res.removeHeader('X-Frame-Options')
32 35
@@ -105,6 +108,24 @@ function serveServerTranslations (req: express.Request, res: express.Response) {
105} 108}
106 109
107async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { 110async function generateEmbedHtmlPage (req: express.Request, res: express.Response) {
111 const hookName = req.originalUrl.startsWith('/video-playlists/')
112 ? 'filter:html.embed.video-playlist.allowed.result'
113 : 'filter:html.embed.video.allowed.result'
114
115 const allowParameters = { req }
116
117 const allowedResult = await Hooks.wrapFun(
118 isEmbedAllowed,
119 allowParameters,
120 hookName
121 )
122
123 if (!allowedResult || allowedResult.allowed !== true) {
124 logger.info('Embed is not allowed.', { allowedResult })
125
126 return sendHTML(allowedResult?.html || '', res)
127 }
128
108 const html = await ClientHtml.getEmbedHTML() 129 const html = await ClientHtml.getEmbedHTML()
109 130
110 return sendHTML(html, res) 131 return sendHTML(html, res)
@@ -158,3 +179,10 @@ function serveClientOverride (path: string) {
158 } 179 }
159 } 180 }
160} 181}
182
183type AllowedResult = { allowed: boolean, html?: string }
184function isEmbedAllowed (_object: {
185 req: express.Request
186}): AllowedResult {
187 return { allowed: true }
188}
diff --git a/server/controllers/download.ts b/server/controllers/download.ts
index 27caa1518..9a8194c5c 100644
--- a/server/controllers/download.ts
+++ b/server/controllers/download.ts
@@ -1,8 +1,10 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { logger } from '@server/helpers/logger'
3import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' 4import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
5import { Hooks } from '@server/lib/plugins/hooks'
4import { getVideoFilePath } from '@server/lib/video-paths' 6import { getVideoFilePath } from '@server/lib/video-paths'
5import { MVideoFile, MVideoFullLight } from '@server/types/models' 7import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
6import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 8import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
7import { VideoStreamingPlaylistType } from '@shared/models' 9import { VideoStreamingPlaylistType } from '@shared/models'
8import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' 10import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
@@ -14,19 +16,19 @@ downloadRouter.use(cors())
14 16
15downloadRouter.use( 17downloadRouter.use(
16 STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', 18 STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename',
17 downloadTorrent 19 asyncMiddleware(downloadTorrent)
18) 20)
19 21
20downloadRouter.use( 22downloadRouter.use(
21 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', 23 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
22 asyncMiddleware(videosDownloadValidator), 24 asyncMiddleware(videosDownloadValidator),
23 downloadVideoFile 25 asyncMiddleware(downloadVideoFile)
24) 26)
25 27
26downloadRouter.use( 28downloadRouter.use(
27 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', 29 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
28 asyncMiddleware(videosDownloadValidator), 30 asyncMiddleware(videosDownloadValidator),
29 downloadHLSVideoFile 31 asyncMiddleware(downloadHLSVideoFile)
30) 32)
31 33
32// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
@@ -41,28 +43,58 @@ async function downloadTorrent (req: express.Request, res: express.Response) {
41 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) 43 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
42 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 44 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
43 45
46 const allowParameters = { torrentPath: result.path, downloadName: result.downloadName }
47
48 const allowedResult = await Hooks.wrapFun(
49 isTorrentDownloadAllowed,
50 allowParameters,
51 'filter:api.download.torrent.allowed.result'
52 )
53
54 if (!checkAllowResult(res, allowParameters, allowedResult)) return
55
44 return res.download(result.path, result.downloadName) 56 return res.download(result.path, result.downloadName)
45} 57}
46 58
47function downloadVideoFile (req: express.Request, res: express.Response) { 59async function downloadVideoFile (req: express.Request, res: express.Response) {
48 const video = res.locals.videoAll 60 const video = res.locals.videoAll
49 61
50 const videoFile = getVideoFile(req, video.VideoFiles) 62 const videoFile = getVideoFile(req, video.VideoFiles)
51 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() 63 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
52 64
65 const allowParameters = { video, videoFile }
66
67 const allowedResult = await Hooks.wrapFun(
68 isVideoDownloadAllowed,
69 allowParameters,
70 'filter:api.download.video.allowed.result'
71 )
72
73 if (!checkAllowResult(res, allowParameters, allowedResult)) return
74
53 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) 75 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
54} 76}
55 77
56function downloadHLSVideoFile (req: express.Request, res: express.Response) { 78async function downloadHLSVideoFile (req: express.Request, res: express.Response) {
57 const video = res.locals.videoAll 79 const video = res.locals.videoAll
58 const playlist = getHLSPlaylist(video) 80 const streamingPlaylist = getHLSPlaylist(video)
59 if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end 81 if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
60 82
61 const videoFile = getVideoFile(req, playlist.VideoFiles) 83 const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles)
62 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() 84 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
63 85
64 const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}` 86 const allowParameters = { video, streamingPlaylist, videoFile }
65 return res.download(getVideoFilePath(playlist, videoFile), filename) 87
88 const allowedResult = await Hooks.wrapFun(
89 isVideoDownloadAllowed,
90 allowParameters,
91 'filter:api.download.video.allowed.result'
92 )
93
94 if (!checkAllowResult(res, allowParameters, allowedResult)) return
95
96 const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}`
97 return res.download(getVideoFilePath(streamingPlaylist, videoFile), filename)
66} 98}
67 99
68function getVideoFile (req: express.Request, files: MVideoFile[]) { 100function getVideoFile (req: express.Request, files: MVideoFile[]) {
@@ -76,3 +108,34 @@ function getHLSPlaylist (video: MVideoFullLight) {
76 108
77 return Object.assign(playlist, { Video: video }) 109 return Object.assign(playlist, { Video: video })
78} 110}
111
112type AllowedResult = {
113 allowed: boolean
114 errorMessage?: string
115}
116
117function isTorrentDownloadAllowed (_object: {
118 torrentPath: string
119}): AllowedResult {
120 return { allowed: true }
121}
122
123function isVideoDownloadAllowed (_object: {
124 video: MVideo
125 videoFile: MVideoFile
126 streamingPlaylist?: MStreamingPlaylist
127}): AllowedResult {
128 return { allowed: true }
129}
130
131function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
132 if (!result || result.allowed !== true) {
133 logger.info('Download is not allowed.', { result, allowParameters })
134 res.status(HttpStatusCode.FORBIDDEN_403)
135 .json({ error: result?.errorMessage || 'Refused download' })
136
137 return false
138 }
139
140 return true
141}
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index e29a8fe1d..921067e65 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,8 +1,9 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Feed from 'pfeed' 2import * as Feed from 'pfeed'
3import { VideoFilter } from '../../shared/models/videos/video-query.type'
3import { buildNSFWFilter } from '../helpers/express-utils' 4import { buildNSFWFilter } from '../helpers/express-utils'
4import { CONFIG } from '../initializers/config' 5import { CONFIG } from '../initializers/config'
5import { FEEDS, ROUTE_CACHE_LIFETIME, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants' 6import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
6import { 7import {
7 asyncMiddleware, 8 asyncMiddleware,
8 commonVideosFiltersValidator, 9 commonVideosFiltersValidator,
@@ -17,7 +18,6 @@ import {
17import { cacheRoute } from '../middlewares/cache' 18import { cacheRoute } from '../middlewares/cache'
18import { VideoModel } from '../models/video/video' 19import { VideoModel } from '../models/video/video'
19import { VideoCommentModel } from '../models/video/video-comment' 20import { VideoCommentModel } from '../models/video/video-comment'
20import { VideoFilter } from '../../shared/models/videos/video-query.type'
21 21
22const feedsRouter = express.Router() 22const feedsRouter = express.Router()
23 23
@@ -318,9 +318,9 @@ function addVideosToFeed (feed, videos: VideoModel[]) {
318 }, 318 },
319 thumbnail: [ 319 thumbnail: [
320 { 320 {
321 url: WEBSERVER.URL + video.getMiniatureStaticPath(), 321 url: WEBSERVER.URL + video.getPreviewStaticPath(),
322 height: THUMBNAILS_SIZE.height, 322 height: PREVIEWS_SIZE.height,
323 width: THUMBNAILS_SIZE.width 323 width: PREVIEWS_SIZE.width
324 } 324 }
325 ] 325 ]
326 }) 326 })
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts
index 4e553479b..6f71fdb16 100644
--- a/server/controllers/lazy-static.ts
+++ b/server/controllers/lazy-static.ts
@@ -4,10 +4,10 @@ import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache
4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' 6import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
7import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar' 7import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image'
8import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' 8import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
9import { asyncMiddleware } from '../middlewares' 9import { asyncMiddleware } from '../middlewares'
10import { AvatarModel } from '../models/avatar/avatar' 10import { ActorImageModel } from '../models/account/actor-image'
11 11
12const lazyStaticRouter = express.Router() 12const lazyStaticRouter = express.Router()
13 13
@@ -15,7 +15,12 @@ lazyStaticRouter.use(cors())
15 15
16lazyStaticRouter.use( 16lazyStaticRouter.use(
17 LAZY_STATIC_PATHS.AVATARS + ':filename', 17 LAZY_STATIC_PATHS.AVATARS + ':filename',
18 asyncMiddleware(getAvatar) 18 asyncMiddleware(getActorImage)
19)
20
21lazyStaticRouter.use(
22 LAZY_STATIC_PATHS.BANNERS + ':filename',
23 asyncMiddleware(getActorImage)
19) 24)
20 25
21lazyStaticRouter.use( 26lazyStaticRouter.use(
@@ -43,36 +48,36 @@ export {
43 48
44// --------------------------------------------------------------------------- 49// ---------------------------------------------------------------------------
45 50
46async function getAvatar (req: express.Request, res: express.Response) { 51async function getActorImage (req: express.Request, res: express.Response) {
47 const filename = req.params.filename 52 const filename = req.params.filename
48 53
49 if (avatarPathUnsafeCache.has(filename)) { 54 if (actorImagePathUnsafeCache.has(filename)) {
50 return res.sendFile(avatarPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) 55 return res.sendFile(actorImagePathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER })
51 } 56 }
52 57
53 const avatar = await AvatarModel.loadByName(filename) 58 const image = await ActorImageModel.loadByName(filename)
54 if (!avatar) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 59 if (!image) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
55 60
56 if (avatar.onDisk === false) { 61 if (image.onDisk === false) {
57 if (!avatar.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 62 if (!image.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
58 63
59 logger.info('Lazy serve remote avatar image %s.', avatar.fileUrl) 64 logger.info('Lazy serve remote actor image %s.', image.fileUrl)
60 65
61 try { 66 try {
62 await pushAvatarProcessInQueue({ filename: avatar.filename, fileUrl: avatar.fileUrl }) 67 await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type })
63 } catch (err) { 68 } catch (err) {
64 logger.warn('Cannot process remote avatar %s.', avatar.fileUrl, { err }) 69 logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err })
65 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 70 return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
66 } 71 }
67 72
68 avatar.onDisk = true 73 image.onDisk = true
69 avatar.save() 74 image.save()
70 .catch(err => logger.error('Cannot save new avatar disk state.', { err })) 75 .catch(err => logger.error('Cannot save new actor image disk state.', { err }))
71 } 76 }
72 77
73 const path = avatar.getPath() 78 const path = image.getPath()
74 79
75 avatarPathUnsafeCache.set(filename, path) 80 actorImagePathUnsafeCache.set(filename, path)
76 return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) 81 return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
77} 82}
78 83
diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts
index 6a1ccc0bf..105f51518 100644
--- a/server/controllers/plugins.ts
+++ b/server/controllers/plugins.ts
@@ -1,15 +1,15 @@
1import * as express from 'express' 1import * as express from 'express'
2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
3import { join } from 'path' 2import { join } from 'path'
4import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' 3import { logger } from '@server/helpers/logger'
5import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins' 4import { optionalAuthenticate } from '@server/middlewares/auth'
6import { serveThemeCSSValidator } from '../middlewares/validators/themes'
7import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
8import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' 5import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
9import { PluginType } from '../../shared/models/plugins/plugin.type' 7import { PluginType } from '../../shared/models/plugins/plugin.type'
10import { isTestInstance } from '../helpers/core-utils' 8import { isTestInstance } from '../helpers/core-utils'
11import { logger } from '@server/helpers/logger' 9import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
12import { optionalAuthenticate } from '@server/middlewares/oauth' 10import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
11import { getExternalAuthValidator, getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
12import { serveThemeCSSValidator } from '../middlewares/validators/themes'
13 13
14const sendFileOptions = { 14const sendFileOptions = {
15 maxAge: '30 days', 15 maxAge: '30 days',
diff --git a/server/controllers/services.ts b/server/controllers/services.ts
index d0217c30a..189e1651b 100644
--- a/server/controllers/services.ts
+++ b/server/controllers/services.ts
@@ -3,6 +3,7 @@ import { EMBED_SIZE, PREVIEWS_SIZE, WEBSERVER, THUMBNAILS_SIZE } from '../initia
3import { asyncMiddleware, oembedValidator } from '../middlewares' 3import { asyncMiddleware, oembedValidator } from '../middlewares'
4import { accountNameWithHostGetValidator } from '../middlewares/validators' 4import { accountNameWithHostGetValidator } from '../middlewares/validators'
5import { MChannelSummary } from '@server/types/models' 5import { MChannelSummary } from '@server/types/models'
6import { escapeHTML } from '@shared/core-utils/renderer'
6 7
7const servicesRouter = express.Router() 8const servicesRouter = express.Router()
8 9
@@ -79,6 +80,7 @@ function buildOEmbed (options: {
79 const embedUrl = webserverUrl + embedPath 80 const embedUrl = webserverUrl + embedPath
80 let embedWidth = EMBED_SIZE.width 81 let embedWidth = EMBED_SIZE.width
81 let embedHeight = EMBED_SIZE.height 82 let embedHeight = EMBED_SIZE.height
83 const embedTitle = escapeHTML(title)
82 84
83 let thumbnailUrl = previewPath 85 let thumbnailUrl = previewPath
84 ? webserverUrl + previewPath 86 ? webserverUrl + previewPath
@@ -96,7 +98,7 @@ function buildOEmbed (options: {
96 } 98 }
97 99
98 const html = `<iframe width="${embedWidth}" height="${embedHeight}" sandbox="allow-same-origin allow-scripts" ` + 100 const html = `<iframe width="${embedWidth}" height="${embedHeight}" sandbox="allow-same-origin allow-scripts" ` +
99 `src="${embedUrl}" frameborder="0" allowfullscreen></iframe>` 101 `title="${embedTitle}" src="${embedUrl}" frameborder="0" allowfullscreen></iframe>`
100 102
101 const json: any = { 103 const json: any = {
102 type: 'video', 104 type: 'video',
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 4baa31117..e6a0628e6 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -252,9 +252,9 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
252 avatar: { 252 avatar: {
253 file: { 253 file: {
254 size: { 254 size: {
255 max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max 255 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
256 }, 256 },
257 extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME 257 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
258 } 258 }
259 }, 259 },
260 video: { 260 video: {