- This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}
+ This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}.
+
+
+
+
This video is blacklisted.
+ {{ video.blacklistedReason }}
@@ -98,6 +103,10 @@
Blacklist
+
+ Unblacklist
+
+
Delete
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index e63ab7bbd..1354de32e 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -1,6 +1,14 @@
@import '_variables';
@import '_mixins';
+.root-row {
+ flex-direction: column;
+}
+
+.blacklisted-label {
+ font-weight: $font-semibold;
+}
+
#video-element-wrapper {
background-color: #000;
display: flex;
@@ -259,6 +267,10 @@
background-image: url('../../../assets/images/video/blacklist.svg');
}
+ &.icon-unblacklist {
+ background-image: url('../../../assets/images/global/undo.svg');
+ }
+
&.icon-delete {
background-image: url('../../../assets/images/global/delete-black.svg');
}
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index 878655d4a..bea13ec99 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -121,7 +121,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.videoCaptionService.listCaptions(uuid)
)
.pipe(
- catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ]))
+ // If 401, the video is private or blacklisted so redirect to 404
+ catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 404 ]))
)
.subscribe(([ video, captionsResult ]) => {
const startTime = this.route.snapshot.queryParams.start
@@ -217,6 +218,31 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.videoBlacklistModal.show()
}
+ async unblacklistVideo (event: Event) {
+ event.preventDefault()
+
+ const confirmMessage = this.i18n(
+ 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
+ )
+
+ const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblacklist'))
+ if (res === false) return
+
+ this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe(
+ () => {
+ this.notificationsService.success(
+ this.i18n('Success'),
+ this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name })
+ )
+
+ this.video.blacklisted = false
+ this.video.blacklistedReason = null
+ },
+
+ err => this.notificationsService.error(this.i18n('Error'), err.message)
+ )
+ }
+
isUserLoggedIn () {
return this.authService.isLoggedIn()
}
@@ -229,6 +255,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
return this.video.isBlackistableBy(this.user)
}
+ isVideoUnblacklistable () {
+ return this.video.isUnblacklistableBy(this.user)
+ }
+
getVideoPoster () {
if (!this.video) return ''
diff --git a/client/src/assets/images/global/undo.svg b/client/src/assets/images/global/undo.svg
new file mode 100644
index 000000000..f1cca03f7
--- /dev/null
+++ b/client/src/assets/images/global/undo.svg
@@ -0,0 +1,11 @@
+
+
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts
index 358f339ed..7f803c8e9 100644
--- a/server/controllers/api/videos/blacklist.ts
+++ b/server/controllers/api/videos/blacklist.ts
@@ -1,5 +1,5 @@
import * as express from 'express'
-import { BlacklistedVideo, UserRight, VideoBlacklistCreate } from '../../../../shared'
+import { VideoBlacklist, UserRight, VideoBlacklistCreate } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils'
import {
@@ -87,7 +87,7 @@ async function updateVideoBlacklistController (req: express.Request, res: expres
async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort)
- return res.json(getFormattedObjects(resultList.data, resultList.total))
+ return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) {
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts
index 203a00876..77d601a4d 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos.ts
@@ -35,6 +35,8 @@ import { VideoShareModel } from '../../models/video/video-share'
import { authenticate } from '../oauth'
import { areValidationErrors } from './utils'
import { cleanUpReqFiles } from '../../helpers/utils'
+import { VideoModel } from '../../models/video/video'
+import { UserModel } from '../../models/account/user'
const videosAddValidator = getCommonVideoAttributes().concat([
body('videofile')
@@ -131,7 +133,25 @@ const videosGetValidator = [
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
- const video = res.locals.video
+ const video: VideoModel = res.locals.video
+
+ // Video private or blacklisted
+ if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
+ authenticate(req, res, () => {
+ const user: UserModel = res.locals.oauth.token.User
+
+ // Only the owner or a user that have blacklist rights can see the video
+ if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
+ return res.status(403)
+ .json({ error: 'Cannot get this private or blacklisted video.' })
+ .end()
+ }
+
+ return next()
+ })
+
+ return
+ }
// Video is public, anyone can access it
if (video.privacy === VideoPrivacy.PUBLIC) return next()
@@ -143,17 +163,6 @@ const videosGetValidator = [
// Don't leak this unlisted video
return res.status(404).end()
}
-
- // Video is private, check the user
- authenticate(req, res, () => {
- if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
- return res.status(403)
- .json({ error: 'Cannot get this private video of another user' })
- .end()
- }
-
- return next()
- })
}
]
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index 10a191372..dbb88ca45 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -137,7 +137,6 @@ export class VideoAbuseModel extends Model {
video: {
id: this.Video.id,
uuid: this.Video.uuid,
- url: this.Video.url,
name: this.Video.name
},
createdAt: this.createdAt
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index 1b8a338cb..eabc37ef0 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -16,7 +16,7 @@ import { getSortOnModel, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
import { Emailer } from '../../lib/emailer'
-import { BlacklistedVideo } from '../../../shared/models/videos'
+import { VideoBlacklist } from '../../../shared/models/videos'
import { CONSTRAINTS_FIELDS } from '../../initializers'
@Table({
@@ -68,7 +68,12 @@ export class VideoBlacklistModel extends Model {
offset: start,
limit: count,
order: getSortOnModel(sort.sortModel, sort.sortValue),
- include: [ { model: VideoModel } ]
+ include: [
+ {
+ model: VideoModel,
+ required: true
+ }
+ ]
}
return VideoBlacklistModel.findAndCountAll(query)
@@ -90,7 +95,7 @@ export class VideoBlacklistModel extends Model {
return VideoBlacklistModel.findOne(query)
}
- toFormattedJSON (): BlacklistedVideo {
+ toFormattedJSON (): VideoBlacklist {
const video = this.Video
return {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index f3a900bc9..b13dee403 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -127,7 +127,8 @@ export enum ScopeNames {
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
WITH_TAGS = 'WITH_TAGS',
WITH_FILES = 'WITH_FILES',
- WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE'
+ WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
+ WITH_BLACKLISTED = 'WITH_BLACKLISTED'
}
type AvailableForListOptions = {
@@ -374,6 +375,15 @@ type AvailableForListOptions = {
[ScopeNames.WITH_TAGS]: {
include: [ () => TagModel ]
},
+ [ScopeNames.WITH_BLACKLISTED]: {
+ include: [
+ {
+ attributes: [ 'id', 'reason' ],
+ model: () => VideoBlacklistModel,
+ required: false
+ }
+ ]
+ },
[ScopeNames.WITH_FILES]: {
include: [
{
@@ -1004,7 +1014,13 @@ export class VideoModel extends Model {
}
return VideoModel
- .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ])
+ .scope([
+ ScopeNames.WITH_TAGS,
+ ScopeNames.WITH_BLACKLISTED,
+ ScopeNames.WITH_FILES,
+ ScopeNames.WITH_ACCOUNT_DETAILS,
+ ScopeNames.WITH_SCHEDULED_UPDATE
+ ])
.findById(id, options)
}
@@ -1030,7 +1046,13 @@ export class VideoModel extends Model {
}
return VideoModel
- .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ])
+ .scope([
+ ScopeNames.WITH_TAGS,
+ ScopeNames.WITH_BLACKLISTED,
+ ScopeNames.WITH_FILES,
+ ScopeNames.WITH_ACCOUNT_DETAILS,
+ ScopeNames.WITH_SCHEDULED_UPDATE
+ ])
.findOne(options)
}
@@ -1276,7 +1298,8 @@ export class VideoModel extends Model {
toFormattedDetailsJSON (): VideoDetails {
const formattedJson = this.toFormattedJSON({
additionalAttributes: {
- scheduledUpdate: true
+ scheduledUpdate: true,
+ blacklistInfo: true
}
})
diff --git a/server/tests/utils/videos/video-blacklist.ts b/server/tests/utils/videos/video-blacklist.ts
index 7819f4b25..2c176fde0 100644
--- a/server/tests/utils/videos/video-blacklist.ts
+++ b/server/tests/utils/videos/video-blacklist.ts
@@ -19,7 +19,8 @@ function updateVideoBlacklist (url: string, token: string, videoId: number, reas
.send({ reason })
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + token)
- .expect(specialStatus)}
+ .expect(specialStatus)
+}
function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = 204) {
const path = '/api/v1/videos/' + videoId + '/blacklist'
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts
index ff6ec61f4..142a0474b 100644
--- a/shared/models/users/user-right.enum.ts
+++ b/shared/models/users/user-right.enum.ts
@@ -1,11 +1,14 @@
export enum UserRight {
ALL,
+
MANAGE_USERS,
MANAGE_SERVER_FOLLOW,
MANAGE_VIDEO_ABUSES,
- MANAGE_VIDEO_BLACKLIST,
MANAGE_JOBS,
MANAGE_CONFIGURATION,
+
+ MANAGE_VIDEO_BLACKLIST,
+
REMOVE_ANY_VIDEO,
REMOVE_ANY_VIDEO_CHANNEL,
REMOVE_ANY_VIDEO_COMMENT,
diff --git a/shared/models/videos/video-abuse.model.ts b/shared/models/videos/video-abuse.model.ts
index 1fecce037..b2319aa00 100644
--- a/shared/models/videos/video-abuse.model.ts
+++ b/shared/models/videos/video-abuse.model.ts
@@ -14,7 +14,6 @@ export interface VideoAbuse {
id: number
name: string
uuid: string
- url: string
}
createdAt: Date
diff --git a/shared/models/videos/video-blacklist.model.ts b/shared/models/videos/video-blacklist.model.ts
index a060da357..ef4e5e3a2 100644
--- a/shared/models/videos/video-blacklist.model.ts
+++ b/shared/models/videos/video-blacklist.model.ts
@@ -1,4 +1,4 @@
-export interface BlacklistedVideo {
+export interface VideoBlacklist {
id: number
createdAt: Date
updatedAt: Date