aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts16
-rw-r--r--server/controllers/api/videos/index.ts2
-rw-r--r--server/helpers/activitypub.ts14
-rw-r--r--server/helpers/requests.ts12
-rw-r--r--server/lib/activitypub/actor.ts9
-rw-r--r--server/lib/activitypub/videos.ts10
-rw-r--r--server/lib/job-queue/handlers/video-import.ts10
-rw-r--r--server/middlewares/cache.ts7
-rw-r--r--server/middlewares/oauth.ts16
-rw-r--r--server/middlewares/validators/videos/videos.ts54
-rw-r--r--server/models/activitypub/actor-follow.ts4
-rw-r--r--server/models/redundancy/video-redundancy.ts7
-rw-r--r--server/models/video/video.ts17
-rw-r--r--server/tests/api/check-params/user-subscriptions.ts6
-rw-r--r--server/tests/api/redundancy/redundancy.ts6
-rw-r--r--server/tests/api/server/follow-constraints.ts215
-rw-r--r--server/tests/api/server/index.ts1
17 files changed, 360 insertions, 46 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index ffbf1ba19..d9d385460 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -39,6 +39,7 @@ import {
39import { VideoCaptionModel } from '../../models/video/video-caption' 39import { VideoCaptionModel } from '../../models/video/video-caption'
40import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy' 40import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy'
41import { getServerActor } from '../../helpers/utils' 41import { getServerActor } from '../../helpers/utils'
42import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
42 43
43const activityPubClientRouter = express.Router() 44const activityPubClientRouter = express.Router()
44 45
@@ -164,6 +165,8 @@ function getAccountVideoRate (rateType: VideoRateType) {
164async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { 165async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
165 const video: VideoModel = res.locals.video 166 const video: VideoModel = res.locals.video
166 167
168 if (video.isOwned() === false) return res.redirect(video.url)
169
167 // We need captions to render AP object 170 // We need captions to render AP object
168 video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id) 171 video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id)
169 172
@@ -180,6 +183,9 @@ async function videoController (req: express.Request, res: express.Response, nex
180 183
181async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { 184async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) {
182 const share = res.locals.videoShare as VideoShareModel 185 const share = res.locals.videoShare as VideoShareModel
186
187 if (share.Actor.isOwned() === false) return res.redirect(share.url)
188
183 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined) 189 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined)
184 190
185 return activityPubResponse(activityPubContextify(activity), res) 191 return activityPubResponse(activityPubContextify(activity), res)
@@ -252,6 +258,8 @@ async function videoChannelFollowingController (req: express.Request, res: expre
252async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) { 258async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) {
253 const videoComment: VideoCommentModel = res.locals.videoComment 259 const videoComment: VideoCommentModel = res.locals.videoComment
254 260
261 if (videoComment.isOwned() === false) return res.redirect(videoComment.url)
262
255 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) 263 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
256 const isPublic = true // Comments are always public 264 const isPublic = true // Comments are always public
257 const audience = getAudience(videoComment.Account.Actor, isPublic) 265 const audience = getAudience(videoComment.Account.Actor, isPublic)
@@ -267,7 +275,9 @@ async function videoCommentController (req: express.Request, res: express.Respon
267} 275}
268 276
269async function videoRedundancyController (req: express.Request, res: express.Response) { 277async function videoRedundancyController (req: express.Request, res: express.Response) {
270 const videoRedundancy = res.locals.videoRedundancy 278 const videoRedundancy: VideoRedundancyModel = res.locals.videoRedundancy
279 if (videoRedundancy.isOwned() === false) return res.redirect(videoRedundancy.url)
280
271 const serverActor = await getServerActor() 281 const serverActor = await getServerActor()
272 282
273 const audience = getAudience(serverActor) 283 const audience = getAudience(serverActor)
@@ -288,7 +298,7 @@ async function actorFollowing (req: express.Request, actor: ActorModel) {
288 return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count) 298 return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
289 } 299 }
290 300
291 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page) 301 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page)
292} 302}
293 303
294async function actorFollowers (req: express.Request, actor: ActorModel) { 304async function actorFollowers (req: express.Request, actor: ActorModel) {
@@ -296,7 +306,7 @@ async function actorFollowers (req: express.Request, actor: ActorModel) {
296 return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count) 306 return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count)
297 } 307 }
298 308
299 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page) 309 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page)
300} 310}
301 311
302function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) { 312function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) {
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index e654bdd09..89fd0432f 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -31,6 +31,7 @@ import {
31 asyncMiddleware, 31 asyncMiddleware,
32 asyncRetryTransactionMiddleware, 32 asyncRetryTransactionMiddleware,
33 authenticate, 33 authenticate,
34 checkVideoFollowConstraints,
34 commonVideosFiltersValidator, 35 commonVideosFiltersValidator,
35 optionalAuthenticate, 36 optionalAuthenticate,
36 paginationValidator, 37 paginationValidator,
@@ -123,6 +124,7 @@ videosRouter.get('/:id/description',
123videosRouter.get('/:id', 124videosRouter.get('/:id',
124 optionalAuthenticate, 125 optionalAuthenticate,
125 asyncMiddleware(videosGetValidator), 126 asyncMiddleware(videosGetValidator),
127 asyncMiddleware(checkVideoFollowConstraints),
126 getVideo 128 getVideo
127) 129)
128videosRouter.post('/:id/views', 130videosRouter.post('/:id/views',
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 4bf6e387d..bcbd9be59 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -57,16 +57,16 @@ function activityPubContextify <T> (data: T) {
57} 57}
58 58
59type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> 59type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
60async function activityPubCollectionPagination (url: string, handler: ActivityPubCollectionPaginationHandler, page?: any) { 60async function activityPubCollectionPagination (baseUrl: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
61 if (!page || !validator.isInt(page)) { 61 if (!page || !validator.isInt(page)) {
62 // We just display the first page URL, we only need the total items 62 // We just display the first page URL, we only need the total items
63 const result = await handler(0, 1) 63 const result = await handler(0, 1)
64 64
65 return { 65 return {
66 id: url, 66 id: baseUrl,
67 type: 'OrderedCollection', 67 type: 'OrderedCollection',
68 totalItems: result.total, 68 totalItems: result.total,
69 first: url + '?page=1' 69 first: baseUrl + '?page=1'
70 } 70 }
71 } 71 }
72 72
@@ -81,19 +81,19 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu
81 81
82 // There are more results 82 // There are more results
83 if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) { 83 if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) {
84 next = url + '?page=' + (page + 1) 84 next = baseUrl + '?page=' + (page + 1)
85 } 85 }
86 86
87 if (page > 1) { 87 if (page > 1) {
88 prev = url + '?page=' + (page - 1) 88 prev = baseUrl + '?page=' + (page - 1)
89 } 89 }
90 90
91 return { 91 return {
92 id: url + '?page=' + page, 92 id: baseUrl + '?page=' + page,
93 type: 'OrderedCollectionPage', 93 type: 'OrderedCollectionPage',
94 prev, 94 prev,
95 next, 95 next,
96 partOf: url, 96 partOf: baseUrl,
97 orderedItems: result.data, 97 orderedItems: result.data,
98 totalItems: result.total 98 totalItems: result.total
99 } 99 }
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index 51facc9e0..805930a9f 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -2,6 +2,7 @@ import * as Bluebird from 'bluebird'
2import { createWriteStream } from 'fs-extra' 2import { createWriteStream } from 'fs-extra'
3import * as request from 'request' 3import * as request from 'request'
4import { ACTIVITY_PUB } from '../initializers' 4import { ACTIVITY_PUB } from '../initializers'
5import { processImage } from './image-utils'
5 6
6function doRequest <T> ( 7function doRequest <T> (
7 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } 8 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
@@ -27,9 +28,18 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U
27 }) 28 })
28} 29}
29 30
31async function downloadImage (url: string, destPath: string, size: { width: number, height: number }) {
32 const tmpPath = destPath + '.tmp'
33
34 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath)
35
36 await processImage({ path: tmpPath }, destPath, size)
37}
38
30// --------------------------------------------------------------------------- 39// ---------------------------------------------------------------------------
31 40
32export { 41export {
33 doRequest, 42 doRequest,
34 doRequestAndSaveToFile 43 doRequestAndSaveToFile,
44 downloadImage
35} 45}
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index b16a00669..218dbc6a7 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -11,9 +11,9 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp
11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../helpers/logger'
13import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 13import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
14import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 14import { doRequest, doRequestAndSaveToFile, downloadImage } from '../../helpers/requests'
15import { getUrlFromWebfinger } from '../../helpers/webfinger' 15import { getUrlFromWebfinger } from '../../helpers/webfinger'
16import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' 16import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, PREVIEWS_SIZE, sequelizeTypescript } from '../../initializers'
17import { AccountModel } from '../../models/account/account' 17import { AccountModel } from '../../models/account/account'
18import { ActorModel } from '../../models/activitypub/actor' 18import { ActorModel } from '../../models/activitypub/actor'
19import { AvatarModel } from '../../models/avatar/avatar' 19import { AvatarModel } from '../../models/avatar/avatar'
@@ -180,10 +180,7 @@ async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
180 const avatarName = uuidv4() + extension 180 const avatarName = uuidv4() + extension
181 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) 181 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
182 182
183 await doRequestAndSaveToFile({ 183 await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE)
184 method: 'GET',
185 uri: actorJSON.icon.url
186 }, destPath)
187 184
188 return avatarName 185 return avatarName
189 } 186 }
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 5bd03c8c6..80de92f24 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -10,8 +10,8 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat
10import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 10import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
11import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 11import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../helpers/logger'
13import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 13import { doRequest, downloadImage } from '../../helpers/requests'
14import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers' 14import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers'
15import { ActorModel } from '../../models/activitypub/actor' 15import { ActorModel } from '../../models/activitypub/actor'
16import { TagModel } from '../../models/video/tag' 16import { TagModel } from '../../models/video/tag'
17import { VideoModel } from '../../models/video/video' 17import { VideoModel } from '../../models/video/video'
@@ -97,11 +97,7 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
97 const thumbnailName = video.getThumbnailName() 97 const thumbnailName = video.getThumbnailName()
98 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) 98 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
99 99
100 const options = { 100 return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE)
101 method: 'GET',
102 uri: icon.url
103 }
104 return doRequestAndSaveToFile(options, thumbnailPath)
105} 101}
106 102
107function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 103function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index e3f2a276c..4de901c0c 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -6,8 +6,8 @@ import { VideoImportState } from '../../../../shared/models/videos'
6import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 6import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
7import { extname, join } from 'path' 7import { extname, join } from 'path'
8import { VideoFileModel } from '../../../models/video/video-file' 8import { VideoFileModel } from '../../../models/video/video-file'
9import { CONFIG, sequelizeTypescript, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' 9import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
10import { doRequestAndSaveToFile } from '../../../helpers/requests' 10import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests'
11import { VideoState } from '../../../../shared' 11import { VideoState } from '../../../../shared'
12import { JobQueue } from '../index' 12import { JobQueue } from '../index'
13import { federateVideoIfNeeded } from '../../activitypub' 13import { federateVideoIfNeeded } from '../../activitypub'
@@ -133,7 +133,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
133 videoId: videoImport.videoId 133 videoId: videoImport.videoId
134 } 134 }
135 videoFile = new VideoFileModel(videoFileData) 135 videoFile = new VideoFileModel(videoFileData)
136 // Import if the import fails, to clean files 136 // To clean files if the import fails
137 videoImport.Video.VideoFiles = [ videoFile ] 137 videoImport.Video.VideoFiles = [ videoFile ]
138 138
139 // Move file 139 // Move file
@@ -145,7 +145,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
145 if (options.downloadThumbnail) { 145 if (options.downloadThumbnail) {
146 if (options.thumbnailUrl) { 146 if (options.thumbnailUrl) {
147 const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName()) 147 const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName())
148 await doRequestAndSaveToFile({ method: 'GET', uri: options.thumbnailUrl }, destThumbnailPath) 148 await downloadImage(options.thumbnailUrl, destThumbnailPath, THUMBNAILS_SIZE)
149 } else { 149 } else {
150 await videoImport.Video.createThumbnail(videoFile) 150 await videoImport.Video.createThumbnail(videoFile)
151 } 151 }
@@ -157,7 +157,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
157 if (options.downloadPreview) { 157 if (options.downloadPreview) {
158 if (options.thumbnailUrl) { 158 if (options.thumbnailUrl) {
159 const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName()) 159 const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName())
160 await doRequestAndSaveToFile({ method: 'GET', uri: options.thumbnailUrl }, destPreviewPath) 160 await downloadImage(options.thumbnailUrl, destPreviewPath, PREVIEWS_SIZE)
161 } else { 161 } else {
162 await videoImport.Video.createPreview(videoFile) 162 await videoImport.Video.createPreview(videoFile)
163 } 163 }
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts
index 1e00fc731..8ffe75700 100644
--- a/server/middlewares/cache.ts
+++ b/server/middlewares/cache.ts
@@ -19,6 +19,7 @@ function cacheRoute (lifetimeArg: string | number) {
19 logger.debug('No cached results for route %s.', req.originalUrl) 19 logger.debug('No cached results for route %s.', req.originalUrl)
20 20
21 const sendSave = res.send.bind(res) 21 const sendSave = res.send.bind(res)
22 const redirectSave = res.redirect.bind(res)
22 23
23 res.send = (body) => { 24 res.send = (body) => {
24 if (res.statusCode >= 200 && res.statusCode < 400) { 25 if (res.statusCode >= 200 && res.statusCode < 400) {
@@ -38,6 +39,12 @@ function cacheRoute (lifetimeArg: string | number) {
38 return sendSave(body) 39 return sendSave(body)
39 } 40 }
40 41
42 res.redirect = url => {
43 done()
44
45 return redirectSave(url)
46 }
47
41 return next() 48 return next()
42 } 49 }
43 50
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts
index 5233b66bd..8c1df2c3e 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/oauth.ts
@@ -28,9 +28,24 @@ function authenticate (req: express.Request, res: express.Response, next: expres
28 }) 28 })
29} 29}
30 30
31function authenticatePromiseIfNeeded (req: express.Request, res: express.Response) {
32 return new Promise(resolve => {
33 // Already authenticated? (or tried to)
34 if (res.locals.oauth && res.locals.oauth.token.User) return resolve()
35
36 if (res.locals.authenticated === false) return res.sendStatus(401)
37
38 authenticate(req, res, () => {
39 return resolve()
40 })
41 })
42}
43
31function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) { 44function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
32 if (req.header('authorization')) return authenticate(req, res, next) 45 if (req.header('authorization')) return authenticate(req, res, next)
33 46
47 res.locals.authenticated = false
48
34 return next() 49 return next()
35} 50}
36 51
@@ -53,6 +68,7 @@ function token (req: express.Request, res: express.Response, next: express.NextF
53 68
54export { 69export {
55 authenticate, 70 authenticate,
71 authenticatePromiseIfNeeded,
56 optionalAuthenticate, 72 optionalAuthenticate,
57 token 73 token
58} 74}
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 656d161d8..051a19e16 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -31,8 +31,8 @@ import {
31} from '../../../helpers/custom-validators/videos' 31} from '../../../helpers/custom-validators/videos'
32import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' 32import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
33import { logger } from '../../../helpers/logger' 33import { logger } from '../../../helpers/logger'
34import { CONSTRAINTS_FIELDS } from '../../../initializers' 34import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers'
35import { authenticate } from '../../oauth' 35import { authenticatePromiseIfNeeded } from '../../oauth'
36import { areValidationErrors } from '../utils' 36import { areValidationErrors } from '../utils'
37import { cleanUpReqFiles } from '../../../helpers/express-utils' 37import { cleanUpReqFiles } from '../../../helpers/express-utils'
38import { VideoModel } from '../../../models/video/video' 38import { VideoModel } from '../../../models/video/video'
@@ -43,6 +43,7 @@ import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ow
43import { AccountModel } from '../../../models/account/account' 43import { AccountModel } from '../../../models/account/account'
44import { VideoFetchType } from '../../../helpers/video' 44import { VideoFetchType } from '../../../helpers/video'
45import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' 45import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
46import { getServerActor } from '../../../helpers/utils'
46 47
47const videosAddValidator = getCommonVideoAttributes().concat([ 48const videosAddValidator = getCommonVideoAttributes().concat([
48 body('videofile') 49 body('videofile')
@@ -127,6 +128,31 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([
127 } 128 }
128]) 129])
129 130
131async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
132 const video: VideoModel = res.locals.video
133
134 // Anybody can watch local videos
135 if (video.isOwned() === true) return next()
136
137 // Logged user
138 if (res.locals.oauth) {
139 // Users can search or watch remote videos
140 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
141 }
142
143 // Anybody can search or watch remote videos
144 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
145
146 // Check our instance follows an actor that shared this video
147 const serverActor = await getServerActor()
148 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
149
150 return res.status(403)
151 .json({
152 error: 'Cannot get this video regarding follow constraints.'
153 })
154}
155
130const videosCustomGetValidator = (fetchType: VideoFetchType) => { 156const videosCustomGetValidator = (fetchType: VideoFetchType) => {
131 return [ 157 return [
132 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 158 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@@ -141,17 +167,20 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
141 167
142 // Video private or blacklisted 168 // Video private or blacklisted
143 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { 169 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
144 return authenticate(req, res, () => { 170 await authenticatePromiseIfNeeded(req, res)
145 const user: UserModel = res.locals.oauth.token.User
146 171
147 // Only the owner or a user that have blacklist rights can see the video 172 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null
148 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
149 return res.status(403)
150 .json({ error: 'Cannot get this private or blacklisted video.' })
151 }
152 173
153 return next() 174 // Only the owner or a user that have blacklist rights can see the video
154 }) 175 if (
176 !user ||
177 (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
178 ) {
179 return res.status(403)
180 .json({ error: 'Cannot get this private or blacklisted video.' })
181 }
182
183 return next()
155 } 184 }
156 185
157 // Video is public, anyone can access it 186 // Video is public, anyone can access it
@@ -376,6 +405,7 @@ export {
376 videosAddValidator, 405 videosAddValidator,
377 videosUpdateValidator, 406 videosUpdateValidator,
378 videosGetValidator, 407 videosGetValidator,
408 checkVideoFollowConstraints,
379 videosCustomGetValidator, 409 videosCustomGetValidator,
380 videosRemoveValidator, 410 videosRemoveValidator,
381 411
@@ -393,6 +423,8 @@ export {
393function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { 423function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
394 if (req.body.scheduleUpdate) { 424 if (req.body.scheduleUpdate) {
395 if (!req.body.scheduleUpdate.updateAt) { 425 if (!req.body.scheduleUpdate.updateAt) {
426 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
427
396 res.status(400) 428 res.status(400)
397 .json({ error: 'Schedule update at is mandatory.' }) 429 .json({ error: 'Schedule update at is mandatory.' })
398 430
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index 3373355ef..0a6935083 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -509,12 +509,12 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
509 tasks.push(ActorFollowModel.sequelize.query(query, options)) 509 tasks.push(ActorFollowModel.sequelize.query(query, options))
510 } 510 }
511 511
512 const [ followers, [ { total } ] ] = await Promise.all(tasks) 512 const [ followers, [ dataTotal ] ] = await Promise.all(tasks)
513 const urls: string[] = followers.map(f => f.url) 513 const urls: string[] = followers.map(f => f.url)
514 514
515 return { 515 return {
516 data: urls, 516 data: urls,
517 total: parseInt(total, 10) 517 total: dataTotal ? parseInt(dataTotal.total, 10) : 0
518 } 518 }
519 } 519 }
520 520
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 35e0cd3b1..9de4356b4 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -117,8 +117,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
117 117
118 @BeforeDestroy 118 @BeforeDestroy
119 static async removeFile (instance: VideoRedundancyModel) { 119 static async removeFile (instance: VideoRedundancyModel) {
120 // Not us 120 if (!instance.isOwned()) return
121 if (!instance.strategy) return
122 121
123 const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) 122 const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
124 123
@@ -404,6 +403,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
404 })) 403 }))
405 } 404 }
406 405
406 isOwned () {
407 return !!this.strategy
408 }
409
407 toActivityPubObject (): CacheFileObject { 410 toActivityPubObject (): CacheFileObject {
408 return { 411 return {
409 id: this.url, 412 id: this.url,
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 6c183933b..1e68b380c 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1253,6 +1253,23 @@ export class VideoModel extends Model<VideoModel> {
1253 }) 1253 })
1254 } 1254 }
1255 1255
1256 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
1257 // Instances only share videos
1258 const query = 'SELECT 1 FROM "videoShare" ' +
1259 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
1260 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' +
1261 'LIMIT 1'
1262
1263 const options = {
1264 type: Sequelize.QueryTypes.SELECT,
1265 bind: { followerActorId, videoId },
1266 raw: true
1267 }
1268
1269 return VideoModel.sequelize.query(query, options)
1270 .then(results => results.length === 1)
1271 }
1272
1256 // threshold corresponds to how many video the field should have to be returned 1273 // threshold corresponds to how many video the field should have to be returned
1257 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1274 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1258 const serverActor = await getServerActor() 1275 const serverActor = await getServerActor()
diff --git a/server/tests/api/check-params/user-subscriptions.ts b/server/tests/api/check-params/user-subscriptions.ts
index 2cf5a2415..8a9ced7c1 100644
--- a/server/tests/api/check-params/user-subscriptions.ts
+++ b/server/tests/api/check-params/user-subscriptions.ts
@@ -14,11 +14,13 @@ import {
14 setAccessTokensToServers, 14 setAccessTokensToServers,
15 userLogin 15 userLogin
16} from '../../../../shared/utils' 16} from '../../../../shared/utils'
17
17import { 18import {
18 checkBadCountPagination, 19 checkBadCountPagination,
19 checkBadSortPagination, 20 checkBadSortPagination,
20 checkBadStartPagination 21 checkBadStartPagination
21} from '../../../../shared/utils/requests/check-api-params' 22} from '../../../../shared/utils/requests/check-api-params'
23import { waitJobs } from '../../../../shared/utils/server/jobs'
22 24
23describe('Test user subscriptions API validators', function () { 25describe('Test user subscriptions API validators', function () {
24 const path = '/api/v1/users/me/subscriptions' 26 const path = '/api/v1/users/me/subscriptions'
@@ -145,6 +147,8 @@ describe('Test user subscriptions API validators', function () {
145 }) 147 })
146 148
147 it('Should succeed with the correct parameters', async function () { 149 it('Should succeed with the correct parameters', async function () {
150 this.timeout(20000)
151
148 await makePostBodyRequest({ 152 await makePostBodyRequest({
149 url: server.url, 153 url: server.url,
150 path, 154 path,
@@ -152,6 +156,8 @@ describe('Test user subscriptions API validators', function () {
152 fields: { uri: 'user1_channel@localhost:9001' }, 156 fields: { uri: 'user1_channel@localhost:9001' },
153 statusCodeExpected: 204 157 statusCodeExpected: 204
154 }) 158 })
159
160 await waitJobs([ server ])
155 }) 161 })
156 }) 162 })
157 163
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index 663e31ead..2bc1b60ce 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -17,9 +17,10 @@ import {
17 viewVideo, 17 viewVideo,
18 wait, 18 wait,
19 waitUntilLog, 19 waitUntilLog,
20 checkVideoFilesWereRemoved, removeVideo 20 checkVideoFilesWereRemoved, removeVideo, getVideoWithToken
21} from '../../../../shared/utils' 21} from '../../../../shared/utils'
22import { waitJobs } from '../../../../shared/utils/server/jobs' 22import { waitJobs } from '../../../../shared/utils/server/jobs'
23
23import * as magnetUtil from 'magnet-uri' 24import * as magnetUtil from 'magnet-uri'
24import { updateRedundancy } from '../../../../shared/utils/server/redundancy' 25import { updateRedundancy } from '../../../../shared/utils/server/redundancy'
25import { ActorFollow } from '../../../../shared/models/actors' 26import { ActorFollow } from '../../../../shared/models/actors'
@@ -93,7 +94,8 @@ async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: str
93 94
94 for (const server of servers) { 95 for (const server of servers) {
95 { 96 {
96 const res = await getVideo(server.url, videoUUID) 97 // With token to avoid issues with video follow constraints
98 const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
97 99
98 const video: VideoDetails = res.body 100 const video: VideoDetails = res.body
99 for (const f of video.files) { 101 for (const f of video.files) {
diff --git a/server/tests/api/server/follow-constraints.ts b/server/tests/api/server/follow-constraints.ts
new file mode 100644
index 000000000..3135fc568
--- /dev/null
+++ b/server/tests/api/server/follow-constraints.ts
@@ -0,0 +1,215 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import { doubleFollow, getAccountVideos, getVideo, getVideoChannelVideos, getVideoWithToken } from '../../utils'
6import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index'
7import { unfollow } from '../../utils/server/follows'
8import { userLogin } from '../../utils/users/login'
9import { createUser } from '../../utils/users/users'
10
11const expect = chai.expect
12
13describe('Test follow constraints', function () {
14 let servers: ServerInfo[] = []
15 let video1UUID: string
16 let video2UUID: string
17 let userAccessToken: string
18
19 before(async function () {
20 this.timeout(30000)
21
22 servers = await flushAndRunMultipleServers(2)
23
24 // Get the access tokens
25 await setAccessTokensToServers(servers)
26
27 {
28 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video server 1' })
29 video1UUID = res.body.video.uuid
30 }
31 {
32 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video server 2' })
33 video2UUID = res.body.video.uuid
34 }
35
36 const user = {
37 username: 'user1',
38 password: 'super_password'
39 }
40 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
41 userAccessToken = await userLogin(servers[0], user)
42
43 await doubleFollow(servers[0], servers[1])
44 })
45
46 describe('With a followed instance', function () {
47
48 describe('With an unlogged user', function () {
49
50 it('Should get the local video', async function () {
51 await getVideo(servers[0].url, video1UUID, 200)
52 })
53
54 it('Should get the remote video', async function () {
55 await getVideo(servers[0].url, video2UUID, 200)
56 })
57
58 it('Should list local account videos', async function () {
59 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5)
60
61 expect(res.body.total).to.equal(1)
62 expect(res.body.data).to.have.lengthOf(1)
63 })
64
65 it('Should list remote account videos', async function () {
66 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5)
67
68 expect(res.body.total).to.equal(1)
69 expect(res.body.data).to.have.lengthOf(1)
70 })
71
72 it('Should list local channel videos', async function () {
73 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5)
74
75 expect(res.body.total).to.equal(1)
76 expect(res.body.data).to.have.lengthOf(1)
77 })
78
79 it('Should list remote channel videos', async function () {
80 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5)
81
82 expect(res.body.total).to.equal(1)
83 expect(res.body.data).to.have.lengthOf(1)
84 })
85 })
86
87 describe('With a logged user', function () {
88 it('Should get the local video', async function () {
89 await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, 200)
90 })
91
92 it('Should get the remote video', async function () {
93 await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, 200)
94 })
95
96 it('Should list local account videos', async function () {
97 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5)
98
99 expect(res.body.total).to.equal(1)
100 expect(res.body.data).to.have.lengthOf(1)
101 })
102
103 it('Should list remote account videos', async function () {
104 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5)
105
106 expect(res.body.total).to.equal(1)
107 expect(res.body.data).to.have.lengthOf(1)
108 })
109
110 it('Should list local channel videos', async function () {
111 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5)
112
113 expect(res.body.total).to.equal(1)
114 expect(res.body.data).to.have.lengthOf(1)
115 })
116
117 it('Should list remote channel videos', async function () {
118 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5)
119
120 expect(res.body.total).to.equal(1)
121 expect(res.body.data).to.have.lengthOf(1)
122 })
123 })
124 })
125
126 describe('With a non followed instance', function () {
127
128 before(async function () {
129 this.timeout(30000)
130
131 await unfollow(servers[0].url, servers[0].accessToken, servers[1])
132 })
133
134 describe('With an unlogged user', function () {
135
136 it('Should get the local video', async function () {
137 await getVideo(servers[0].url, video1UUID, 200)
138 })
139
140 it('Should not get the remote video', async function () {
141 await getVideo(servers[0].url, video2UUID, 403)
142 })
143
144 it('Should list local account videos', async function () {
145 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5)
146
147 expect(res.body.total).to.equal(1)
148 expect(res.body.data).to.have.lengthOf(1)
149 })
150
151 it('Should not list remote account videos', async function () {
152 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5)
153
154 expect(res.body.total).to.equal(0)
155 expect(res.body.data).to.have.lengthOf(0)
156 })
157
158 it('Should list local channel videos', async function () {
159 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5)
160
161 expect(res.body.total).to.equal(1)
162 expect(res.body.data).to.have.lengthOf(1)
163 })
164
165 it('Should not list remote channel videos', async function () {
166 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5)
167
168 expect(res.body.total).to.equal(0)
169 expect(res.body.data).to.have.lengthOf(0)
170 })
171 })
172
173 describe('With a logged user', function () {
174 it('Should get the local video', async function () {
175 await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, 200)
176 })
177
178 it('Should get the remote video', async function () {
179 await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, 200)
180 })
181
182 it('Should list local account videos', async function () {
183 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5)
184
185 expect(res.body.total).to.equal(1)
186 expect(res.body.data).to.have.lengthOf(1)
187 })
188
189 it('Should list remote account videos', async function () {
190 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5)
191
192 expect(res.body.total).to.equal(1)
193 expect(res.body.data).to.have.lengthOf(1)
194 })
195
196 it('Should list local channel videos', async function () {
197 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5)
198
199 expect(res.body.total).to.equal(1)
200 expect(res.body.data).to.have.lengthOf(1)
201 })
202
203 it('Should list remote channel videos', async function () {
204 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5)
205
206 expect(res.body.total).to.equal(1)
207 expect(res.body.data).to.have.lengthOf(1)
208 })
209 })
210 })
211
212 after(async function () {
213 killallServers(servers)
214 })
215})
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
index 78ab7e18b..6afcab1f9 100644
--- a/server/tests/api/server/index.ts
+++ b/server/tests/api/server/index.ts
@@ -1,5 +1,6 @@
1import './config' 1import './config'
2import './email' 2import './email'
3import './follow-constraints'
3import './follows' 4import './follows'
4import './handle-down' 5import './handle-down'
5import './jobs' 6import './jobs'