aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/videos/shared/video.model.ts8
-rw-r--r--client/src/app/videos/shared/video.service.ts6
-rw-r--r--client/src/app/videos/video-list/video-list.component.ts5
-rw-r--r--client/src/app/videos/video-list/video-miniature.component.html4
-rw-r--r--client/src/app/videos/video-list/video-miniature.component.scss14
-rw-r--r--client/src/app/videos/video-list/video-miniature.component.ts20
-rw-r--r--client/src/app/videos/video-watch/video-watch.component.html12
-rw-r--r--client/src/app/videos/video-watch/video-watch.component.ts47
-rw-r--r--server/controllers/api/videos.js25
-rw-r--r--server/middlewares/validators/videos.js63
-rw-r--r--server/models/user.js8
-rw-r--r--server/models/video-blacklist.js89
-rw-r--r--server/models/video.js48
13 files changed, 283 insertions, 66 deletions
diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/videos/shared/video.model.ts
index 404e3bf45..1cfb312b6 100644
--- a/client/src/app/videos/shared/video.model.ts
+++ b/client/src/app/videos/shared/video.model.ts
@@ -85,8 +85,12 @@ export class Video {
85 this.by = Video.createByString(hash.author, hash.podHost); 85 this.by = Video.createByString(hash.author, hash.podHost);
86 } 86 }
87 87
88 isRemovableBy(user: User) { 88 isRemovableBy(user) {
89 return this.isLocal === true && user && this.author === user.username; 89 return user && this.isLocal === true && (this.author === user.username || user.isAdmin() === true);
90 }
91
92 isBlackistableBy(user) {
93 return user && user.isAdmin() === true && this.isLocal === false;
90 } 94 }
91 95
92 isVideoNSFWForUser(user: User) { 96 isVideoNSFWForUser(user: User) {
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts
index ee67bc1ae..a0965e20c 100644
--- a/client/src/app/videos/shared/video.service.ts
+++ b/client/src/app/videos/shared/video.service.ts
@@ -150,6 +150,12 @@ export class VideoService {
150 .catch((res) => this.restExtractor.handleError(res)); 150 .catch((res) => this.restExtractor.handleError(res));
151 } 151 }
152 152
153 blacklistVideo(id: string) {
154 return this.authHttp.post(VideoService.BASE_VIDEO_URL + id + '/blacklist', {})
155 .map(this.restExtractor.extractDataBool)
156 .catch((res) => this.restExtractor.handleError(res));
157 }
158
153 private setVideoRate(id: string, rateType: RateType) { 159 private setVideoRate(id: string, rateType: RateType) {
154 const url = VideoService.BASE_VIDEO_URL + id + '/rate'; 160 const url = VideoService.BASE_VIDEO_URL + id + '/rate';
155 const body = { 161 const body = {
diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts
index ede1b51a9..b9f19b4f1 100644
--- a/client/src/app/videos/video-list/video-list.component.ts
+++ b/client/src/app/videos/video-list/video-list.component.ts
@@ -108,11 +108,6 @@ export class VideoListComponent implements OnInit, OnDestroy {
108 this.navigateToNewParams(); 108 this.navigateToNewParams();
109 } 109 }
110 110
111 onRemoved(video: Video) {
112 this.notificationsService.success('Success', `Video ${video.name} deleted.`);
113 this.getVideos();
114 }
115
116 onSort(sort: SortField) { 111 onSort(sort: SortField) {
117 this.sort = sort; 112 this.sort = sort;
118 113
diff --git a/client/src/app/videos/video-list/video-miniature.component.html b/client/src/app/videos/video-list/video-miniature.component.html
index 94b892698..b8b448631 100644
--- a/client/src/app/videos/video-list/video-miniature.component.html
+++ b/client/src/app/videos/video-list/video-miniature.component.html
@@ -10,10 +10,6 @@
10 10
11 <span class="video-miniature-duration">{{ video.duration }}</span> 11 <span class="video-miniature-duration">{{ video.duration }}</span>
12 </a> 12 </a>
13 <span
14 *ngIf="displayRemoveIcon()" (click)="removeVideo(video.id)"
15 class="video-miniature-remove glyphicon glyphicon-remove"
16 ></span>
17 13
18 <div class="video-miniature-informations"> 14 <div class="video-miniature-informations">
19 <span class="video-miniature-name-tags"> 15 <span class="video-miniature-name-tags">
diff --git a/client/src/app/videos/video-list/video-miniature.component.scss b/client/src/app/videos/video-list/video-miniature.component.scss
index b8e90e8c5..f88ced819 100644
--- a/client/src/app/videos/video-list/video-miniature.component.scss
+++ b/client/src/app/videos/video-list/video-miniature.component.scss
@@ -42,20 +42,6 @@
42 } 42 }
43 } 43 }
44 44
45 .video-miniature-remove {
46 display: inline-block;
47 position: absolute;
48 left: 16px;
49 background-color: rgba(0, 0, 0, 0.8);
50 color: rgba(255, 255, 255, 0.8);
51 padding: 2px;
52 cursor: pointer;
53
54 &:hover {
55 color: rgba(255, 255, 255, 0.9);
56 }
57 }
58
59 .video-miniature-informations { 45 .video-miniature-informations {
60 width: 200px; 46 width: 200px;
61 47
diff --git a/client/src/app/videos/video-list/video-miniature.component.ts b/client/src/app/videos/video-list/video-miniature.component.ts
index 888026dde..13deec381 100644
--- a/client/src/app/videos/video-list/video-miniature.component.ts
+++ b/client/src/app/videos/video-list/video-miniature.component.ts
@@ -13,8 +13,6 @@ import { User } from '../../shared';
13}) 13})
14 14
15export class VideoMiniatureComponent { 15export class VideoMiniatureComponent {
16 @Output() removed = new EventEmitter<any>();
17
18 @Input() currentSort: SortField; 16 @Input() currentSort: SortField;
19 @Input() user: User; 17 @Input() user: User;
20 @Input() video: Video; 18 @Input() video: Video;
@@ -28,10 +26,6 @@ export class VideoMiniatureComponent {
28 private videoService: VideoService 26 private videoService: VideoService
29 ) {} 27 ) {}
30 28
31 displayRemoveIcon() {
32 return this.hovering && this.video.isRemovableBy(this.user);
33 }
34
35 getVideoName() { 29 getVideoName() {
36 if (this.isVideoNSFWForThisUser()) 30 if (this.isVideoNSFWForThisUser())
37 return 'NSFW'; 31 return 'NSFW';
@@ -47,20 +41,6 @@ export class VideoMiniatureComponent {
47 this.hovering = true; 41 this.hovering = true;
48 } 42 }
49 43
50 removeVideo(id: string) {
51 this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe(
52 res => {
53 if (res === false) return;
54
55 this.videoService.removeVideo(id).subscribe(
56 status => this.removed.emit(true),
57
58 error => this.notificationsService.error('Error', error.text)
59 );
60 }
61 );
62 }
63
64 isVideoNSFWForThisUser() { 44 isVideoNSFWForThisUser() {
65 return this.video.isVideoNSFWForUser(this.user); 45 return this.video.isVideoNSFWForUser(this.user);
66 } 46 }
diff --git a/client/src/app/videos/video-watch/video-watch.component.html b/client/src/app/videos/video-watch/video-watch.component.html
index 19e9bd9ed..ed26b513e 100644
--- a/client/src/app/videos/video-watch/video-watch.component.html
+++ b/client/src/app/videos/video-watch/video-watch.component.html
@@ -96,6 +96,18 @@
96 <span class="glyphicon glyphicon-alert"></span> Report 96 <span class="glyphicon glyphicon-alert"></span> Report
97 </a> 97 </a>
98 </li> 98 </li>
99
100 <li *ngIf="isVideoRemovable()" role="menuitem">
101 <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)">
102 <span class="glyphicon glyphicon-remove"></span> Delete
103 </a>
104 </li>
105
106 <li *ngIf="isVideoBlacklistable()" role="menuitem">
107 <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
108 <span class="glyphicon glyphicon-eye-close"></span> Blacklist
109 </a>
110 </li>
99 </ul> 111 </ul>
100 </div> 112 </div>
101 </div> 113 </div>
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 e04626a67..f582df45c 100644
--- a/client/src/app/videos/video-watch/video-watch.component.ts
+++ b/client/src/app/videos/video-watch/video-watch.component.ts
@@ -169,6 +169,45 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
169 ); 169 );
170 } 170 }
171 171
172 removeVideo(event: Event) {
173 event.preventDefault();
174 this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe(
175 res => {
176 if (res === false) return;
177
178 this.videoService.removeVideo(this.video.id)
179 .subscribe(
180 status => {
181 this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
182 // Go back to the video-list.
183 this.router.navigate(['/videos/list'])
184 },
185
186 error => this.notificationsService.error('Error', error.text)
187 );
188 }
189 );
190 }
191
192 blacklistVideo(event: Event) {
193 event.preventDefault()
194 this.confirmService.confirm('Do you really want to blacklist this video ?', 'Blacklist').subscribe(
195 res => {
196 if (res === false) return;
197
198 this.videoService.blacklistVideo(this.video.id)
199 .subscribe(
200 status => {
201 this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
202 this.router.navigate(['/videos/list'])
203 },
204
205 error => this.notificationsService.error('Error', error.text)
206 )
207 }
208 )
209 }
210
172 showReportModal(event: Event) { 211 showReportModal(event: Event) {
173 event.preventDefault(); 212 event.preventDefault();
174 this.videoReportModal.show(); 213 this.videoReportModal.show();
@@ -192,6 +231,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
192 this.authService.getUser().username === this.video.author; 231 this.authService.getUser().username === this.video.author;
193 } 232 }
194 233
234 isVideoRemovable() {
235 return this.video.isRemovableBy(this.authService.getUser());
236 }
237
238 isVideoBlacklistable() {
239 return this.video.isBlackistableBy(this.authService.getUser());
240 }
241
195 private checkUserRating() { 242 private checkUserRating() {
196 // Unlogged users do not have ratings 243 // Unlogged users do not have ratings
197 if (this.isUserLoggedIn() === false) return; 244 if (this.isUserLoggedIn() === false) return;
diff --git a/server/controllers/api/videos.js b/server/controllers/api/videos.js
index 5e9ff482f..1f7d30eef 100644
--- a/server/controllers/api/videos.js
+++ b/server/controllers/api/videos.js
@@ -93,11 +93,13 @@ router.get('/:id',
93 validatorsVideos.videosGet, 93 validatorsVideos.videosGet,
94 getVideo 94 getVideo
95) 95)
96
96router.delete('/:id', 97router.delete('/:id',
97 oAuth.authenticate, 98 oAuth.authenticate,
98 validatorsVideos.videosRemove, 99 validatorsVideos.videosRemove,
99 removeVideo 100 removeVideo
100) 101)
102
101router.get('/search/:value', 103router.get('/search/:value',
102 validatorsVideos.videosSearch, 104 validatorsVideos.videosSearch,
103 validatorsPagination.pagination, 105 validatorsPagination.pagination,
@@ -108,6 +110,13 @@ router.get('/search/:value',
108 searchVideos 110 searchVideos
109) 111)
110 112
113router.post('/:id/blacklist',
114 oAuth.authenticate,
115 admin.ensureIsAdmin,
116 validatorsVideos.videosBlacklist,
117 addVideoToBlacklist
118)
119
111// --------------------------------------------------------------------------- 120// ---------------------------------------------------------------------------
112 121
113module.exports = router 122module.exports = router
@@ -622,3 +631,19 @@ function reportVideoAbuse (req, res, finalCallback) {
622 return finalCallback(null) 631 return finalCallback(null)
623 }) 632 })
624} 633}
634
635function addVideoToBlacklist (req, res, next) {
636 const videoInstance = res.locals.video
637
638 db.BlacklistedVideo.create({
639 videoId: videoInstance.id
640 })
641 .asCallback(function (err) {
642 if (err) {
643 logger.error('Errors when blacklisting video ', { error: err })
644 return next(err)
645 }
646
647 return res.type('json').status(204).end()
648 })
649}
diff --git a/server/middlewares/validators/videos.js b/server/middlewares/validators/videos.js
index c07825e50..86a7e39ae 100644
--- a/server/middlewares/validators/videos.js
+++ b/server/middlewares/validators/videos.js
@@ -15,7 +15,9 @@ const validatorsVideos = {
15 15
16 videoAbuseReport, 16 videoAbuseReport,
17 17
18 videoRate 18 videoRate,
19
20 videosBlacklist
19} 21}
20 22
21function videosAdd (req, res, next) { 23function videosAdd (req, res, next) {
@@ -95,15 +97,10 @@ function videosRemove (req, res, next) {
95 checkVideoExists(req.params.id, res, function () { 97 checkVideoExists(req.params.id, res, function () {
96 // We need to make additional checks 98 // We need to make additional checks
97 99
98 if (res.locals.video.isOwned() === false) { 100 // Check if the user who did the request is able to delete the video
99 return res.status(403).send('Cannot remove video of another pod') 101 checkUserCanDeleteVideo(res.locals.oauth.token.User.id, res, function () {
100 } 102 next()
101 103 })
102 if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) {
103 return res.status(403).send('Cannot remove video of another user')
104 }
105
106 next()
107 }) 104 })
108 }) 105 })
109} 106}
@@ -159,3 +156,49 @@ function checkVideoExists (id, res, callback) {
159 callback() 156 callback()
160 }) 157 })
161} 158}
159
160function checkUserCanDeleteVideo (userId, res, callback) {
161 // Retrieve the user who did the request
162 db.User.loadById(userId, function (err, user) {
163 if (err) {
164 logger.error('Error in video request validator.', { error: err })
165 return res.sendStatus(500)
166 }
167
168 // Check if the user can delete the video
169 // The user can delete it if s/he an admin
170 // Or if s/he is the video's author
171 if (user.isAdmin() === false) {
172 if (res.locals.video.isOwned() === false) {
173 return res.status(403).send('Cannot remove video of another pod')
174 }
175
176 if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) {
177 return res.status(403).send('Cannot remove video of another user')
178 }
179 }
180
181 // If we reach this comment, we can delete the video
182 callback()
183 })
184}
185
186function checkVideoIsBlacklistable (req, res, callback) {
187 if (res.locals.video.isOwned() === true) {
188 return res.status(403).send('Cannot blacklist a local video')
189 }
190
191 callback()
192}
193
194function videosBlacklist (req, res, next) {
195 req.checkParams('id', 'Should have a valid id').notEmpty().isUUID(4)
196
197 logger.debug('Checking videosBlacklist parameters', { parameters: req.params })
198
199 checkErrors(req, res, function () {
200 checkVideoExists(req.params.id, res, function() {
201 checkVideoIsBlacklistable(req, res, next)
202 })
203 })
204}
diff --git a/server/models/user.js b/server/models/user.js
index e64bab8ab..8f9c2bf65 100644
--- a/server/models/user.js
+++ b/server/models/user.js
@@ -79,7 +79,8 @@ module.exports = function (sequelize, DataTypes) {
79 }, 79 },
80 instanceMethods: { 80 instanceMethods: {
81 isPasswordMatch, 81 isPasswordMatch,
82 toFormatedJSON 82 toFormatedJSON,
83 isAdmin
83 }, 84 },
84 hooks: { 85 hooks: {
85 beforeCreate: beforeCreateOrUpdate, 86 beforeCreate: beforeCreateOrUpdate,
@@ -117,6 +118,11 @@ function toFormatedJSON () {
117 createdAt: this.createdAt 118 createdAt: this.createdAt
118 } 119 }
119} 120}
121
122function isAdmin () {
123 return this.role === constants.USER_ROLES.ADMIN
124}
125
120// ------------------------------ STATICS ------------------------------ 126// ------------------------------ STATICS ------------------------------
121 127
122function associate (models) { 128function associate (models) {
diff --git a/server/models/video-blacklist.js b/server/models/video-blacklist.js
new file mode 100644
index 000000000..02ea15760
--- /dev/null
+++ b/server/models/video-blacklist.js
@@ -0,0 +1,89 @@
1'use strict'
2
3const modelUtils = require('./utils')
4
5// ---------------------------------------------------------------------------
6
7module.exports = function (sequelize, DataTypes) {
8 const BlacklistedVideo = sequelize.define('BlacklistedVideo',
9 {},
10 {
11 indexes: [
12 {
13 fields: [ 'videoId' ],
14 unique: true
15 }
16 ],
17 classMethods: {
18 associate,
19
20 countTotal,
21 list,
22 listForApi,
23 loadById,
24 loadByVideoId
25 },
26 instanceMethods: {
27 toFormatedJSON
28 },
29 hooks: {}
30 }
31 )
32
33 return BlacklistedVideo
34}
35
36// ------------------------------ METHODS ------------------------------
37
38function toFormatedJSON () {
39 return {
40 id: this.id,
41 videoId: this.videoId,
42 createdAt: this.createdAt
43 }
44}
45
46// ------------------------------ STATICS ------------------------------
47
48function associate (models) {
49 this.belongsTo(models.Video, {
50 foreignKey: 'videoId',
51 onDelete: 'cascade'
52 })
53}
54
55function countTotal (callback) {
56 return this.count().asCallback(callback)
57}
58
59function list (callback) {
60 return this.findAll().asCallback(callback)
61}
62
63function listForApi (start, count, sort, callback) {
64 const query = {
65 offset: start,
66 limit: count,
67 order: [ modelUtils.getSort(sort) ]
68 }
69
70 return this.findAndCountAll(query).asCallback(function (err, result) {
71 if (err) return callback(err)
72
73 return callback(null, result.rows, result.count)
74 })
75}
76
77function loadById (id, callback) {
78 return this.findById(id).asCallback(callback)
79}
80
81function loadByVideoId (id, callback) {
82 const query = {
83 where: {
84 videoId: id
85 }
86 }
87
88 return this.find(query).asCallback(callback)
89}
diff --git a/server/models/video.js b/server/models/video.js
index 39eb28ed9..1addfa682 100644
--- a/server/models/video.js
+++ b/server/models/video.js
@@ -16,6 +16,7 @@ const logger = require('../helpers/logger')
16const friends = require('../lib/friends') 16const friends = require('../lib/friends')
17const modelUtils = require('./utils') 17const modelUtils = require('./utils')
18const customVideosValidators = require('../helpers/custom-validators').videos 18const customVideosValidators = require('../helpers/custom-validators').videos
19const db = require('../initializers/database')
19 20
20// --------------------------------------------------------------------------- 21// ---------------------------------------------------------------------------
21 22
@@ -201,7 +202,8 @@ module.exports = function (sequelize, DataTypes) {
201 isOwned, 202 isOwned,
202 toFormatedJSON, 203 toFormatedJSON,
203 toAddRemoteJSON, 204 toAddRemoteJSON,
204 toUpdateRemoteJSON 205 toUpdateRemoteJSON,
206 removeFromBlacklist
205 }, 207 },
206 hooks: { 208 hooks: {
207 beforeValidate, 209 beforeValidate,
@@ -528,6 +530,7 @@ function list (callback) {
528} 530}
529 531
530function listForApi (start, count, sort, callback) { 532function listForApi (start, count, sort, callback) {
533 // Exclude Blakclisted videos from the list
531 const query = { 534 const query = {
532 offset: start, 535 offset: start,
533 limit: count, 536 limit: count,
@@ -540,7 +543,12 @@ function listForApi (start, count, sort, callback) {
540 }, 543 },
541 544
542 this.sequelize.models.Tag 545 this.sequelize.models.Tag
543 ] 546 ],
547 where: {
548 id: { $notIn: this.sequelize.literal(
549 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
550 )}
551 }
544 } 552 }
545 553
546 return this.findAndCountAll(query).asCallback(function (err, result) { 554 return this.findAndCountAll(query).asCallback(function (err, result) {
@@ -648,7 +656,11 @@ function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort,
648 } 656 }
649 657
650 const query = { 658 const query = {
651 where: {}, 659 where: {
660 id: { $notIn: this.sequelize.literal(
661 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
662 )}
663 },
652 offset: start, 664 offset: start,
653 limit: count, 665 limit: count,
654 distinct: true, // For the count, a video can have many tags 666 distinct: true, // For the count, a video can have many tags
@@ -661,13 +673,9 @@ function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort,
661 query.where.infoHash = infoHash 673 query.where.infoHash = infoHash
662 } else if (field === 'tags') { 674 } else if (field === 'tags') {
663 const escapedValue = this.sequelize.escape('%' + value + '%') 675 const escapedValue = this.sequelize.escape('%' + value + '%')
664 query.where = { 676 query.where.id.$in = this.sequelize.literal(
665 id: { 677 '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')'
666 $in: this.sequelize.literal( 678 )
667 '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')'
668 )
669 }
670 }
671 } else if (field === 'host') { 679 } else if (field === 'host') {
672 // FIXME: Include our pod? (not stored in the database) 680 // FIXME: Include our pod? (not stored in the database)
673 podInclude.where = { 681 podInclude.where = {
@@ -755,3 +763,23 @@ function generateImage (video, videoPath, folder, imageName, size, callback) {
755 }) 763 })
756 .thumbnail(options) 764 .thumbnail(options)
757} 765}
766
767function removeFromBlacklist (video, callback) {
768 // Find the blacklisted video
769 db.BlacklistedVideo.loadByVideoId(video.id, function (err, video) {
770 // If an error occured, stop here
771 if (err) {
772 logger.error('Error when fetching video from blacklist.', { error: err })
773
774 return callback(err)
775 }
776
777 // If we found the video, remove it from the blacklist
778 if (video) {
779 video.destroy().asCallback(callback)
780 } else {
781 // If haven't found it, simply ignore it and do nothing
782 return callback()
783 }
784 })
785}