diff options
author | Chocobozzz <me@florianbigard.com> | 2019-12-12 15:47:47 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-12-12 16:51:59 +0100 |
commit | 22a73cb879a5cc775d4bec3d72fa9c9cf52e5175 (patch) | |
tree | 4c8d2d4f6fce8a520420ec83722fefc6d57b7a83 | |
parent | 91fa7960f42cff3481465bece3389007fbc278d3 (diff) | |
download | PeerTube-22a73cb879a5cc775d4bec3d72fa9c9cf52e5175.tar.gz PeerTube-22a73cb879a5cc775d4bec3d72fa9c9cf52e5175.tar.zst PeerTube-22a73cb879a5cc775d4bec3d72fa9c9cf52e5175.zip |
Add internal privacy mode
-rw-r--r-- | client/src/app/shared/video/modals/video-download.component.ts | 2 | ||||
-rw-r--r-- | client/src/app/shared/video/video.service.ts | 30 | ||||
-rw-r--r-- | server/controllers/api/videos/index.ts | 24 | ||||
-rw-r--r-- | server/controllers/api/videos/ownership.ts | 4 | ||||
-rw-r--r-- | server/helpers/custom-validators/videos.ts | 2 | ||||
-rw-r--r-- | server/initializers/constants.ts | 3 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-create.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-update.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/share.ts | 5 | ||||
-rw-r--r-- | server/lib/activitypub/videos.ts | 2 | ||||
-rw-r--r-- | server/lib/client-html.ts | 2 | ||||
-rw-r--r-- | server/lib/schedulers/update-videos-scheduler.ts | 10 | ||||
-rw-r--r-- | server/middlewares/validators/videos/videos.ts | 9 | ||||
-rw-r--r-- | server/models/account/user.ts | 18 | ||||
-rw-r--r-- | server/models/video/schedule-video-update.ts | 2 | ||||
-rw-r--r-- | server/models/video/video.ts | 55 | ||||
-rw-r--r-- | server/tests/api/videos/video-privacy.ts | 128 | ||||
-rw-r--r-- | shared/models/videos/video-privacy.enum.ts | 3 | ||||
-rw-r--r-- | shared/models/videos/video-schedule-update.model.ts | 2 |
19 files changed, 217 insertions, 88 deletions
diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts index 5849ee458..712740086 100644 --- a/client/src/app/shared/video/modals/video-download.component.ts +++ b/client/src/app/shared/video/modals/video-download.component.ts | |||
@@ -59,7 +59,7 @@ export class VideoDownloadComponent { | |||
59 | return | 59 | return |
60 | } | 60 | } |
61 | 61 | ||
62 | const suffix = this.video.privacy.id === VideoPrivacy.PRIVATE | 62 | const suffix = this.video.privacy.id === VideoPrivacy.PRIVATE || this.video.privacy.id === VideoPrivacy.INTERNAL |
63 | ? '?access_token=' + this.auth.getAccessToken() | 63 | ? '?access_token=' + this.auth.getAccessToken() |
64 | : '' | 64 | : '' |
65 | 65 | ||
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 45366e3e3..b0fa55966 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts | |||
@@ -332,18 +332,26 @@ export class VideoService implements VideosProvider { | |||
332 | } | 332 | } |
333 | 333 | ||
334 | explainedPrivacyLabels (privacies: VideoConstant<VideoPrivacy>[]) { | 334 | explainedPrivacyLabels (privacies: VideoConstant<VideoPrivacy>[]) { |
335 | const newPrivacies = privacies.slice() | 335 | const base = [ |
336 | 336 | { | |
337 | const privatePrivacy = newPrivacies.find(p => p.id === VideoPrivacy.PRIVATE) | 337 | id: VideoPrivacy.PRIVATE, |
338 | if (privatePrivacy) privatePrivacy.label = this.i18n('Only I can see this video') | 338 | label: this.i18n('Only I can see this video') |
339 | 339 | }, | |
340 | const unlistedPrivacy = newPrivacies.find(p => p.id === VideoPrivacy.UNLISTED) | 340 | { |
341 | if (unlistedPrivacy) unlistedPrivacy.label = this.i18n('Only people with the private link can see this video') | 341 | id: VideoPrivacy.UNLISTED, |
342 | 342 | label: this.i18n('Only people with the private link can see this video') | |
343 | const publicPrivacy = newPrivacies.find(p => p.id === VideoPrivacy.PUBLIC) | 343 | }, |
344 | if (publicPrivacy) publicPrivacy.label = this.i18n('Anyone can see this video') | 344 | { |
345 | id: VideoPrivacy.PUBLIC, | ||
346 | label: this.i18n('Anyone can see this video') | ||
347 | }, | ||
348 | { | ||
349 | id: VideoPrivacy.INTERNAL, | ||
350 | label: this.i18n('Only users of this instance can see this video') | ||
351 | } | ||
352 | ] | ||
345 | 353 | ||
346 | return privacies | 354 | return base.filter(o => !!privacies.find(p => p.id === o.id)) |
347 | } | 355 | } |
348 | 356 | ||
349 | private setVideoRate (id: number, rateType: UserVideoRateType) { | 357 | private setVideoRate (id: number, rateType: UserVideoRateType) { |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 337795541..35f0b3152 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -326,9 +326,8 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
326 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) | 326 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) |
327 | const videoInfoToUpdate: VideoUpdate = req.body | 327 | const videoInfoToUpdate: VideoUpdate = req.body |
328 | 328 | ||
329 | const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE | 329 | const wasConfidentialVideo = videoInstance.isConfidential() |
330 | const wasNotPrivateVideo = videoInstance.privacy !== VideoPrivacy.PRIVATE | 330 | const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation() |
331 | const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED | ||
332 | 331 | ||
333 | // Process thumbnail or create it from the video | 332 | // Process thumbnail or create it from the video |
334 | const thumbnailModel = req.files && req.files['thumbnailfile'] | 333 | const thumbnailModel = req.files && req.files['thumbnailfile'] |
@@ -359,17 +358,15 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
359 | videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt) | 358 | videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt) |
360 | } | 359 | } |
361 | 360 | ||
361 | let isNewVideo = false | ||
362 | if (videoInfoToUpdate.privacy !== undefined) { | 362 | if (videoInfoToUpdate.privacy !== undefined) { |
363 | const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10) | 363 | isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy) |
364 | videoInstance.privacy = newPrivacy | ||
365 | 364 | ||
366 | // The video was private, and is not anymore -> publish it | 365 | const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10) |
367 | if (wasPrivateVideo === true && newPrivacy !== VideoPrivacy.PRIVATE) { | 366 | videoInstance.setPrivacy(newPrivacy) |
368 | videoInstance.publishedAt = new Date() | ||
369 | } | ||
370 | 367 | ||
371 | // The video was not private, but now it is -> we need to unfederate it | 368 | // Unfederate the video if the new privacy is not compatible with federation |
372 | if (wasNotPrivateVideo === true && newPrivacy === VideoPrivacy.PRIVATE) { | 369 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { |
373 | await VideoModel.sendDelete(videoInstance, { transaction: t }) | 370 | await VideoModel.sendDelete(videoInstance, { transaction: t }) |
374 | } | 371 | } |
375 | } | 372 | } |
@@ -392,7 +389,7 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
392 | await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) | 389 | await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) |
393 | videoInstanceUpdated.VideoChannel = res.locals.videoChannel | 390 | videoInstanceUpdated.VideoChannel = res.locals.videoChannel |
394 | 391 | ||
395 | if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) | 392 | if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) |
396 | } | 393 | } |
397 | 394 | ||
398 | // Schedule an update in the future? | 395 | // Schedule an update in the future? |
@@ -414,7 +411,6 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
414 | transaction: t | 411 | transaction: t |
415 | }) | 412 | }) |
416 | 413 | ||
417 | const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE | ||
418 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | 414 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) |
419 | 415 | ||
420 | auditLogger.update( | 416 | auditLogger.update( |
@@ -427,7 +423,7 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
427 | return videoInstanceUpdated | 423 | return videoInstanceUpdated |
428 | }) | 424 | }) |
429 | 425 | ||
430 | if (wasUnlistedVideo || wasPrivateVideo) { | 426 | if (wasConfidentialVideo) { |
431 | Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated) | 427 | Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated) |
432 | } | 428 | } |
433 | 429 | ||
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts index abb34082e..41d7cdc43 100644 --- a/server/controllers/api/videos/ownership.ts +++ b/server/controllers/api/videos/ownership.ts | |||
@@ -12,7 +12,7 @@ import { | |||
12 | videosTerminateChangeOwnershipValidator | 12 | videosTerminateChangeOwnershipValidator |
13 | } from '../../../middlewares' | 13 | } from '../../../middlewares' |
14 | import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' | 14 | import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' |
15 | import { VideoChangeOwnershipStatus, VideoPrivacy, VideoState } from '../../../../shared/models/videos' | 15 | import { VideoChangeOwnershipStatus, VideoState } from '../../../../shared/models/videos' |
16 | import { VideoChannelModel } from '../../../models/video/video-channel' | 16 | import { VideoChannelModel } from '../../../models/video/video-channel' |
17 | import { getFormattedObjects } from '../../../helpers/utils' | 17 | import { getFormattedObjects } from '../../../helpers/utils' |
18 | import { changeVideoChannelShare } from '../../../lib/activitypub' | 18 | import { changeVideoChannelShare } from '../../../lib/activitypub' |
@@ -111,7 +111,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) { | |||
111 | const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight | 111 | const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight |
112 | targetVideoUpdated.VideoChannel = channel | 112 | targetVideoUpdated.VideoChannel = channel |
113 | 113 | ||
114 | if (targetVideoUpdated.privacy !== VideoPrivacy.PRIVATE && targetVideoUpdated.state === VideoState.PUBLISHED) { | 114 | if (targetVideoUpdated.hasPrivacyForFederation() && targetVideoUpdated.state === VideoState.PUBLISHED) { |
115 | await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t) | 115 | await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t) |
116 | await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor) | 116 | await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor) |
117 | } | 117 | } |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index e92ef9b92..ff93f2e99 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -102,7 +102,7 @@ function isVideoPrivacyValid (value: number) { | |||
102 | } | 102 | } |
103 | 103 | ||
104 | function isScheduleVideoUpdatePrivacyValid (value: number) { | 104 | function isScheduleVideoUpdatePrivacyValid (value: number) { |
105 | return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC | 105 | return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC || value === VideoPrivacy.INTERNAL |
106 | } | 106 | } |
107 | 107 | ||
108 | function isVideoOriginallyPublishedAtValid (value: string | null) { | 108 | function isVideoOriginallyPublishedAtValid (value: string | null) { |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index bdabe7f66..d0cf4d5de 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -353,7 +353,8 @@ let VIDEO_LANGUAGES: { [id: string]: string } = {} | |||
353 | const VIDEO_PRIVACIES = { | 353 | const VIDEO_PRIVACIES = { |
354 | [ VideoPrivacy.PUBLIC ]: 'Public', | 354 | [ VideoPrivacy.PUBLIC ]: 'Public', |
355 | [ VideoPrivacy.UNLISTED ]: 'Unlisted', | 355 | [ VideoPrivacy.UNLISTED ]: 'Unlisted', |
356 | [ VideoPrivacy.PRIVATE ]: 'Private' | 356 | [ VideoPrivacy.PRIVATE ]: 'Private', |
357 | [ VideoPrivacy.INTERNAL ]: 'Internal' | ||
357 | } | 358 | } |
358 | 359 | ||
359 | const VIDEO_STATES = { | 360 | const VIDEO_STATES = { |
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index edbc14a73..1709d8348 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -18,7 +18,7 @@ import { | |||
18 | } from '../../../typings/models' | 18 | } from '../../../typings/models' |
19 | 19 | ||
20 | async function sendCreateVideo (video: MVideoAP, t: Transaction) { | 20 | async function sendCreateVideo (video: MVideoAP, t: Transaction) { |
21 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined | 21 | if (!video.hasPrivacyForFederation()) return undefined |
22 | 22 | ||
23 | logger.info('Creating job to send video creation of %s.', video.url) | 23 | logger.info('Creating job to send video creation of %s.', video.url) |
24 | 24 | ||
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index 44e0e1161..cb14b8dbf 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts | |||
@@ -25,7 +25,7 @@ import { | |||
25 | async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) { | 25 | async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) { |
26 | const video = videoArg as MVideoAP | 26 | const video = videoArg as MVideoAP |
27 | 27 | ||
28 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined | 28 | if (!video.hasPrivacyForFederation()) return undefined |
29 | 29 | ||
30 | logger.info('Creating job to update video %s.', video.url) | 30 | logger.info('Creating job to update video %s.', video.url) |
31 | 31 | ||
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index fdca9bed7..e847c4b7d 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { VideoPrivacy } from '../../../shared/models/videos' | ||
3 | import { getServerActor } from '../../helpers/utils' | 2 | import { getServerActor } from '../../helpers/utils' |
4 | import { VideoShareModel } from '../../models/video/video-share' | 3 | import { VideoShareModel } from '../../models/video/video-share' |
5 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' | 4 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' |
@@ -10,10 +9,10 @@ import { getOrCreateActorAndServerAndModel } from './actor' | |||
10 | import { logger } from '../../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
11 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 10 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
12 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | 11 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
13 | import { MChannelActor, MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video' | 12 | import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video' |
14 | 13 | ||
15 | async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { | 14 | async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { |
16 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined | 15 | if (!video.hasPrivacyForFederation()) return undefined |
17 | 16 | ||
18 | return Promise.all([ | 17 | return Promise.all([ |
19 | shareByServer(video, t), | 18 | shareByServer(video, t), |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index d80173e03..2fb1f8d49 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -79,7 +79,7 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid | |||
79 | // Check this is not a blacklisted video, or unfederated blacklisted video | 79 | // Check this is not a blacklisted video, or unfederated blacklisted video |
80 | (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && | 80 | (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && |
81 | // Check the video is public/unlisted and published | 81 | // Check the video is public/unlisted and published |
82 | video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED | 82 | video.hasPrivacyForFederation() && video.state === VideoState.PUBLISHED |
83 | ) { | 83 | ) { |
84 | // Fetch more attributes that we will need to serialize in AP object | 84 | // Fetch more attributes that we will need to serialize in AP object |
85 | if (isArray(video.VideoCaptions) === false) { | 85 | if (isArray(video.VideoCaptions) === false) { |
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index a1f4ae858..42a30f84f 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -46,7 +46,7 @@ export class ClientHtml { | |||
46 | ]) | 46 | ]) |
47 | 47 | ||
48 | // Let Angular application handle errors | 48 | // Let Angular application handle errors |
49 | if (!video || video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { | 49 | if (!video || video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL || video.VideoBlacklist) { |
50 | return ClientHtml.getIndexHTML(req, res) | 50 | return ClientHtml.getIndexHTML(req, res) |
51 | } | 51 | } |
52 | 52 | ||
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts index 293bba91f..350a335d3 100644 --- a/server/lib/schedulers/update-videos-scheduler.ts +++ b/server/lib/schedulers/update-videos-scheduler.ts | |||
@@ -35,16 +35,14 @@ export class UpdateVideosScheduler extends AbstractScheduler { | |||
35 | logger.info('Executing scheduled video update on %s.', video.uuid) | 35 | logger.info('Executing scheduled video update on %s.', video.uuid) |
36 | 36 | ||
37 | if (schedule.privacy) { | 37 | if (schedule.privacy) { |
38 | const oldPrivacy = video.privacy | 38 | const wasConfidentialVideo = video.isConfidential() |
39 | const isNewVideo = oldPrivacy === VideoPrivacy.PRIVATE | 39 | const isNewVideo = video.isNewVideo(schedule.privacy) |
40 | |||
41 | video.privacy = schedule.privacy | ||
42 | if (isNewVideo === true) video.publishedAt = new Date() | ||
43 | 40 | ||
41 | video.setPrivacy(schedule.privacy) | ||
44 | await video.save({ transaction: t }) | 42 | await video.save({ transaction: t }) |
45 | await federateVideoIfNeeded(video, isNewVideo, t) | 43 | await federateVideoIfNeeded(video, isNewVideo, t) |
46 | 44 | ||
47 | if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) { | 45 | if (wasConfidentialVideo) { |
48 | const videoToPublish: MVideoFullLight = Object.assign(video, { ScheduleVideoUpdate: schedule, UserVideoHistories: [] }) | 46 | const videoToPublish: MVideoFullLight = Object.assign(video, { ScheduleVideoUpdate: schedule, UserVideoHistories: [] }) |
49 | publishedVideos.push(videoToPublish) | 47 | publishedVideos.push(videoToPublish) |
50 | } | 48 | } |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index ab984d84a..5e0182cc3 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -161,18 +161,15 @@ const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video- | |||
161 | const videoAll = video as MVideoFullLight | 161 | const videoAll = video as MVideoFullLight |
162 | 162 | ||
163 | // Video private or blacklisted | 163 | // Video private or blacklisted |
164 | if (video.privacy === VideoPrivacy.PRIVATE || videoAll.VideoBlacklist) { | 164 | if (videoAll.requiresAuth()) { |
165 | await authenticatePromiseIfNeeded(req, res, authenticateInQuery) | 165 | await authenticatePromiseIfNeeded(req, res, authenticateInQuery) |
166 | 166 | ||
167 | const user = res.locals.oauth ? res.locals.oauth.token.User : null | 167 | const user = res.locals.oauth ? res.locals.oauth.token.User : null |
168 | 168 | ||
169 | // Only the owner or a user that have blacklist rights can see the video | 169 | // Only the owner or a user that have blacklist rights can see the video |
170 | if ( | 170 | if (!user || !user.canGetVideo(videoAll)) { |
171 | !user || | ||
172 | (videoAll.VideoChannel && videoAll.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) | ||
173 | ) { | ||
174 | return res.status(403) | 171 | return res.status(403) |
175 | .json({ error: 'Cannot get this private or blacklisted video.' }) | 172 | .json({ error: 'Cannot get this private/internal or blacklisted video.' }) |
176 | } | 173 | } |
177 | 174 | ||
178 | return next() | 175 | return next() |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 38c6d474a..522ea3310 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -19,7 +19,7 @@ import { | |||
19 | Table, | 19 | Table, |
20 | UpdatedAt | 20 | UpdatedAt |
21 | } from 'sequelize-typescript' | 21 | } from 'sequelize-typescript' |
22 | import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' | 22 | import { hasUserRight, USER_ROLE_LABELS, UserRight, VideoPrivacy } from '../../../shared' |
23 | import { User, UserRole } from '../../../shared/models/users' | 23 | import { User, UserRole } from '../../../shared/models/users' |
24 | import { | 24 | import { |
25 | isNoInstanceConfigWarningModal, | 25 | isNoInstanceConfigWarningModal, |
@@ -63,7 +63,7 @@ import { | |||
63 | MUserFormattable, | 63 | MUserFormattable, |
64 | MUserId, | 64 | MUserId, |
65 | MUserNotifSettingChannelDefault, | 65 | MUserNotifSettingChannelDefault, |
66 | MUserWithNotificationSetting | 66 | MUserWithNotificationSetting, MVideoFullLight |
67 | } from '@server/typings/models' | 67 | } from '@server/typings/models' |
68 | 68 | ||
69 | enum ScopeNames { | 69 | enum ScopeNames { |
@@ -575,6 +575,20 @@ export class UserModel extends Model<UserModel> { | |||
575 | .then(u => u.map(u => u.username)) | 575 | .then(u => u.map(u => u.username)) |
576 | } | 576 | } |
577 | 577 | ||
578 | canGetVideo (video: MVideoFullLight) { | ||
579 | if (video.privacy === VideoPrivacy.INTERNAL) return true | ||
580 | |||
581 | if (video.privacy === VideoPrivacy.PRIVATE) { | ||
582 | return video.VideoChannel && video.VideoChannel.Account.userId === this.id | ||
583 | } | ||
584 | |||
585 | if (video.isBlacklisted()) { | ||
586 | return this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) | ||
587 | } | ||
588 | |||
589 | return false | ||
590 | } | ||
591 | |||
578 | hasRight (right: UserRight) { | 592 | hasRight (right: UserRight) { |
579 | return hasUserRight(this.role, right) | 593 | return hasUserRight(this.role, right) |
580 | } | 594 | } |
diff --git a/server/models/video/schedule-video-update.ts b/server/models/video/schedule-video-update.ts index eefc10f14..00b7f5524 100644 --- a/server/models/video/schedule-video-update.ts +++ b/server/models/video/schedule-video-update.ts | |||
@@ -26,7 +26,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> { | |||
26 | @AllowNull(true) | 26 | @AllowNull(true) |
27 | @Default(null) | 27 | @Default(null) |
28 | @Column | 28 | @Column |
29 | privacy: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED | 29 | privacy: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED | VideoPrivacy.INTERNAL |
30 | 30 | ||
31 | @CreatedAt | 31 | @CreatedAt |
32 | createdAt: Date | 32 | createdAt: Date |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index af6fae0b6..7e18af497 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -348,9 +348,8 @@ export type AvailableForListIDsOptions = { | |||
348 | 348 | ||
349 | // Only list public/published videos | 349 | // Only list public/published videos |
350 | if (!options.filter || options.filter !== 'all-local') { | 350 | if (!options.filter || options.filter !== 'all-local') { |
351 | const privacyWhere = { | 351 | |
352 | // Always list public videos | 352 | const publishWhere = { |
353 | privacy: VideoPrivacy.PUBLIC, | ||
354 | // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding | 353 | // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding |
355 | [ Op.or ]: [ | 354 | [ Op.or ]: [ |
356 | { | 355 | { |
@@ -364,8 +363,26 @@ export type AvailableForListIDsOptions = { | |||
364 | } | 363 | } |
365 | ] | 364 | ] |
366 | } | 365 | } |
366 | whereAnd.push(publishWhere) | ||
367 | 367 | ||
368 | whereAnd.push(privacyWhere) | 368 | // List internal videos if the user is logged in |
369 | if (options.user) { | ||
370 | const privacyWhere = { | ||
371 | [Op.or]: [ | ||
372 | { | ||
373 | privacy: VideoPrivacy.INTERNAL | ||
374 | }, | ||
375 | { | ||
376 | privacy: VideoPrivacy.PUBLIC | ||
377 | } | ||
378 | ] | ||
379 | } | ||
380 | |||
381 | whereAnd.push(privacyWhere) | ||
382 | } else { // Or only public videos | ||
383 | const privacyWhere = { privacy: VideoPrivacy.PUBLIC } | ||
384 | whereAnd.push(privacyWhere) | ||
385 | } | ||
369 | } | 386 | } |
370 | 387 | ||
371 | if (options.videoPlaylistId) { | 388 | if (options.videoPlaylistId) { |
@@ -1773,6 +1790,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1773 | } | 1790 | } |
1774 | } | 1791 | } |
1775 | 1792 | ||
1793 | private static isPrivacyForFederation (privacy: VideoPrivacy) { | ||
1794 | return privacy === VideoPrivacy.PUBLIC || privacy === VideoPrivacy.UNLISTED | ||
1795 | } | ||
1796 | |||
1776 | static getCategoryLabel (id: number) { | 1797 | static getCategoryLabel (id: number) { |
1777 | return VIDEO_CATEGORIES[ id ] || 'Misc' | 1798 | return VIDEO_CATEGORIES[ id ] || 'Misc' |
1778 | } | 1799 | } |
@@ -1980,12 +2001,38 @@ export class VideoModel extends Model<VideoModel> { | |||
1980 | return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL) | 2001 | return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL) |
1981 | } | 2002 | } |
1982 | 2003 | ||
2004 | hasPrivacyForFederation () { | ||
2005 | return VideoModel.isPrivacyForFederation(this.privacy) | ||
2006 | } | ||
2007 | |||
2008 | isNewVideo (newPrivacy: VideoPrivacy) { | ||
2009 | return this.hasPrivacyForFederation() === false && VideoModel.isPrivacyForFederation(newPrivacy) === true | ||
2010 | } | ||
2011 | |||
1983 | setAsRefreshed () { | 2012 | setAsRefreshed () { |
1984 | this.changed('updatedAt', true) | 2013 | this.changed('updatedAt', true) |
1985 | 2014 | ||
1986 | return this.save() | 2015 | return this.save() |
1987 | } | 2016 | } |
1988 | 2017 | ||
2018 | requiresAuth () { | ||
2019 | return this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL || !!this.VideoBlacklist | ||
2020 | } | ||
2021 | |||
2022 | setPrivacy (newPrivacy: VideoPrivacy) { | ||
2023 | if (this.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) { | ||
2024 | this.publishedAt = new Date() | ||
2025 | } | ||
2026 | |||
2027 | this.privacy = newPrivacy | ||
2028 | } | ||
2029 | |||
2030 | isConfidential () { | ||
2031 | return this.privacy === VideoPrivacy.PRIVATE || | ||
2032 | this.privacy === VideoPrivacy.UNLISTED || | ||
2033 | this.privacy === VideoPrivacy.INTERNAL | ||
2034 | } | ||
2035 | |||
1989 | async publishIfNeededAndSave (t: Transaction) { | 2036 | async publishIfNeededAndSave (t: Transaction) { |
1990 | if (this.state !== VideoState.PUBLISHED) { | 2037 | if (this.state !== VideoState.PUBLISHED) { |
1991 | this.state = VideoState.PUBLISHED | 2038 | this.state = VideoState.PUBLISHED |
diff --git a/server/tests/api/videos/video-privacy.ts b/server/tests/api/videos/video-privacy.ts index 40b539106..e630ca84a 100644 --- a/server/tests/api/videos/video-privacy.ts +++ b/server/tests/api/videos/video-privacy.ts | |||
@@ -16,14 +16,22 @@ import { userLogin } from '../../../../shared/extra-utils/users/login' | |||
16 | import { createUser } from '../../../../shared/extra-utils/users/users' | 16 | import { createUser } from '../../../../shared/extra-utils/users/users' |
17 | import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/extra-utils/videos/videos' | 17 | import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/extra-utils/videos/videos' |
18 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 18 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
19 | import { Video } from '@shared/models' | ||
19 | 20 | ||
20 | const expect = chai.expect | 21 | const expect = chai.expect |
21 | 22 | ||
22 | describe('Test video privacy', function () { | 23 | describe('Test video privacy', function () { |
23 | let servers: ServerInfo[] = [] | 24 | let servers: ServerInfo[] = [] |
25 | let anotherUserToken: string | ||
26 | |||
24 | let privateVideoId: number | 27 | let privateVideoId: number |
25 | let privateVideoUUID: string | 28 | let privateVideoUUID: string |
29 | |||
30 | let internalVideoId: number | ||
31 | let internalVideoUUID: string | ||
32 | |||
26 | let unlistedVideoUUID: string | 33 | let unlistedVideoUUID: string |
34 | |||
27 | let now: number | 35 | let now: number |
28 | 36 | ||
29 | before(async function () { | 37 | before(async function () { |
@@ -39,39 +47,63 @@ describe('Test video privacy', function () { | |||
39 | await doubleFollow(servers[0], servers[1]) | 47 | await doubleFollow(servers[0], servers[1]) |
40 | }) | 48 | }) |
41 | 49 | ||
42 | it('Should upload a private video on server 1', async function () { | 50 | it('Should upload a private and internal videos on server 1', async function () { |
43 | this.timeout(10000) | 51 | this.timeout(10000) |
44 | 52 | ||
45 | const attributes = { | 53 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { |
46 | privacy: VideoPrivacy.PRIVATE | 54 | const attributes = { privacy } |
55 | await uploadVideo(servers[0].url, servers[0].accessToken, attributes) | ||
47 | } | 56 | } |
48 | await uploadVideo(servers[0].url, servers[0].accessToken, attributes) | ||
49 | 57 | ||
50 | await waitJobs(servers) | 58 | await waitJobs(servers) |
51 | }) | 59 | }) |
52 | 60 | ||
53 | it('Should not have this private video on server 2', async function () { | 61 | it('Should not have these private and internal videos on server 2', async function () { |
54 | const res = await getVideosList(servers[1].url) | 62 | const res = await getVideosList(servers[1].url) |
55 | 63 | ||
56 | expect(res.body.total).to.equal(0) | 64 | expect(res.body.total).to.equal(0) |
57 | expect(res.body.data).to.have.lengthOf(0) | 65 | expect(res.body.data).to.have.lengthOf(0) |
58 | }) | 66 | }) |
59 | 67 | ||
60 | it('Should list my (private) videos', async function () { | 68 | it('Should not list the private and internal videos for an unauthenticated user on server 1', async function () { |
61 | const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 1) | 69 | const res = await getVideosList(servers[0].url) |
70 | |||
71 | expect(res.body.total).to.equal(0) | ||
72 | expect(res.body.data).to.have.lengthOf(0) | ||
73 | }) | ||
74 | |||
75 | it('Should not list the private video and list the internal video for an authenticated user on server 1', async function () { | ||
76 | const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) | ||
62 | 77 | ||
63 | expect(res.body.total).to.equal(1) | 78 | expect(res.body.total).to.equal(1) |
64 | expect(res.body.data).to.have.lengthOf(1) | 79 | expect(res.body.data).to.have.lengthOf(1) |
65 | 80 | ||
66 | privateVideoId = res.body.data[0].id | 81 | expect(res.body.data[0].privacy.id).to.equal(VideoPrivacy.INTERNAL) |
67 | privateVideoUUID = res.body.data[0].uuid | 82 | }) |
83 | |||
84 | it('Should list my (private and internal) videos', async function () { | ||
85 | const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 10) | ||
86 | |||
87 | expect(res.body.total).to.equal(2) | ||
88 | expect(res.body.data).to.have.lengthOf(2) | ||
89 | |||
90 | const videos: Video[] = res.body.data | ||
91 | |||
92 | const privateVideo = videos.find(v => v.privacy.id === VideoPrivacy.PRIVATE) | ||
93 | privateVideoId = privateVideo.id | ||
94 | privateVideoUUID = privateVideo.uuid | ||
95 | |||
96 | const internalVideo = videos.find(v => v.privacy.id === VideoPrivacy.INTERNAL) | ||
97 | internalVideoId = internalVideo.id | ||
98 | internalVideoUUID = internalVideo.uuid | ||
68 | }) | 99 | }) |
69 | 100 | ||
70 | it('Should not be able to watch this video with non authenticated user', async function () { | 101 | it('Should not be able to watch the private/internal video with non authenticated user', async function () { |
71 | await getVideo(servers[0].url, privateVideoUUID, 401) | 102 | await getVideo(servers[0].url, privateVideoUUID, 401) |
103 | await getVideo(servers[0].url, internalVideoUUID, 401) | ||
72 | }) | 104 | }) |
73 | 105 | ||
74 | it('Should not be able to watch this private video with another user', async function () { | 106 | it('Should not be able to watch the private video with another user', async function () { |
75 | this.timeout(10000) | 107 | this.timeout(10000) |
76 | 108 | ||
77 | const user = { | 109 | const user = { |
@@ -80,12 +112,16 @@ describe('Test video privacy', function () { | |||
80 | } | 112 | } |
81 | await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password }) | 113 | await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password }) |
82 | 114 | ||
83 | const token = await userLogin(servers[0], user) | 115 | anotherUserToken = await userLogin(servers[0], user) |
84 | await getVideoWithToken(servers[0].url, token, privateVideoUUID, 403) | 116 | await getVideoWithToken(servers[0].url, anotherUserToken, privateVideoUUID, 403) |
85 | }) | 117 | }) |
86 | 118 | ||
87 | it('Should be able to watch this video with the correct user', async function () { | 119 | it('Should be able to watch the internal video with another user', async function () { |
88 | await getVideoWithToken(servers[0].url, servers[0].accessToken, privateVideoUUID) | 120 | await getVideoWithToken(servers[0].url, anotherUserToken, internalVideoUUID, 200) |
121 | }) | ||
122 | |||
123 | it('Should be able to watch the private video with the correct user', async function () { | ||
124 | await getVideoWithToken(servers[0].url, servers[0].accessToken, privateVideoUUID, 200) | ||
89 | }) | 125 | }) |
90 | 126 | ||
91 | it('Should upload an unlisted video on server 2', async function () { | 127 | it('Should upload an unlisted video on server 2', async function () { |
@@ -127,16 +163,27 @@ describe('Test video privacy', function () { | |||
127 | } | 163 | } |
128 | }) | 164 | }) |
129 | 165 | ||
130 | it('Should update the private video to public on server 1', async function () { | 166 | it('Should update the private and internal videos to public on server 1', async function () { |
131 | this.timeout(10000) | 167 | this.timeout(10000) |
132 | 168 | ||
133 | const attribute = { | 169 | now = Date.now() |
134 | name: 'super video public', | 170 | |
135 | privacy: VideoPrivacy.PUBLIC | 171 | { |
172 | const attribute = { | ||
173 | name: 'private video becomes public', | ||
174 | privacy: VideoPrivacy.PUBLIC | ||
175 | } | ||
176 | |||
177 | await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, privateVideoId, attribute) | ||
136 | } | 178 | } |
137 | 179 | ||
138 | now = Date.now() | 180 | { |
139 | await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, attribute) | 181 | const attribute = { |
182 | name: 'internal video becomes public', | ||
183 | privacy: VideoPrivacy.PUBLIC | ||
184 | } | ||
185 | await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, internalVideoId, attribute) | ||
186 | } | ||
140 | 187 | ||
141 | await waitJobs(servers) | 188 | await waitJobs(servers) |
142 | }) | 189 | }) |
@@ -144,18 +191,30 @@ describe('Test video privacy', function () { | |||
144 | it('Should have this new public video listed on server 1 and 2', async function () { | 191 | it('Should have this new public video listed on server 1 and 2', async function () { |
145 | for (const server of servers) { | 192 | for (const server of servers) { |
146 | const res = await getVideosList(server.url) | 193 | const res = await getVideosList(server.url) |
194 | expect(res.body.total).to.equal(2) | ||
195 | expect(res.body.data).to.have.lengthOf(2) | ||
196 | |||
197 | const videos: Video[] = res.body.data | ||
198 | const privateVideo = videos.find(v => v.name === 'private video becomes public') | ||
199 | const internalVideo = videos.find(v => v.name === 'internal video becomes public') | ||
200 | |||
201 | expect(privateVideo).to.not.be.undefined | ||
202 | expect(internalVideo).to.not.be.undefined | ||
203 | |||
204 | expect(new Date(privateVideo.publishedAt).getTime()).to.be.at.least(now) | ||
205 | // We don't change the publish date of internal videos | ||
206 | expect(new Date(internalVideo.publishedAt).getTime()).to.be.below(now) | ||
147 | 207 | ||
148 | expect(res.body.total).to.equal(1) | 208 | expect(privateVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) |
149 | expect(res.body.data).to.have.lengthOf(1) | 209 | expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) |
150 | expect(res.body.data[0].name).to.equal('super video public') | ||
151 | expect(new Date(res.body.data[0].publishedAt).getTime()).to.be.at.least(now) | ||
152 | } | 210 | } |
153 | }) | 211 | }) |
154 | 212 | ||
155 | it('Should set this new video as private', async function () { | 213 | it('Should set these videos as private and internal', async function () { |
156 | this.timeout(10000) | 214 | this.timeout(10000) |
157 | 215 | ||
158 | await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.PRIVATE }) | 216 | await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, { privacy: VideoPrivacy.PRIVATE }) |
217 | await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.INTERNAL }) | ||
159 | 218 | ||
160 | await waitJobs(servers) | 219 | await waitJobs(servers) |
161 | 220 | ||
@@ -168,10 +227,19 @@ describe('Test video privacy', function () { | |||
168 | 227 | ||
169 | { | 228 | { |
170 | const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5) | 229 | const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5) |
230 | const videos = res.body.data | ||
231 | |||
232 | expect(res.body.total).to.equal(2) | ||
233 | expect(videos).to.have.lengthOf(2) | ||
234 | |||
235 | const privateVideo = videos.find(v => v.name === 'private video becomes public') | ||
236 | const internalVideo = videos.find(v => v.name === 'internal video becomes public') | ||
237 | |||
238 | expect(privateVideo).to.not.be.undefined | ||
239 | expect(internalVideo).to.not.be.undefined | ||
171 | 240 | ||
172 | expect(res.body.total).to.equal(1) | 241 | expect(privateVideo.privacy.id).to.equal(VideoPrivacy.INTERNAL) |
173 | expect(res.body.data).to.have.lengthOf(1) | 242 | expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PRIVATE) |
174 | expect(res.body.data[0].name).to.equal('super video public') | ||
175 | } | 243 | } |
176 | }) | 244 | }) |
177 | 245 | ||
diff --git a/shared/models/videos/video-privacy.enum.ts b/shared/models/videos/video-privacy.enum.ts index 29888c7b8..17ed0c9bb 100644 --- a/shared/models/videos/video-privacy.enum.ts +++ b/shared/models/videos/video-privacy.enum.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export enum VideoPrivacy { | 1 | export enum VideoPrivacy { |
2 | PUBLIC = 1, | 2 | PUBLIC = 1, |
3 | UNLISTED = 2, | 3 | UNLISTED = 2, |
4 | PRIVATE = 3 | 4 | PRIVATE = 3, |
5 | INTERNAL = 4 | ||
5 | } | 6 | } |
diff --git a/shared/models/videos/video-schedule-update.model.ts b/shared/models/videos/video-schedule-update.model.ts index b865c1614..87d74f654 100644 --- a/shared/models/videos/video-schedule-update.model.ts +++ b/shared/models/videos/video-schedule-update.model.ts | |||
@@ -2,5 +2,5 @@ import { VideoPrivacy } from './video-privacy.enum' | |||
2 | 2 | ||
3 | export interface VideoScheduleUpdate { | 3 | export interface VideoScheduleUpdate { |
4 | updateAt: Date | string | 4 | updateAt: Date | string |
5 | privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED // Cannot schedule an update to PRIVATE | 5 | privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED | VideoPrivacy.INTERNAL // Cannot schedule an update to PRIVATE |
6 | } | 6 | } |