* Add ability for an admin to remove every video on the pod.
* Server: add BlacklistedVideos relation.
* Server: Insert in BlacklistedVideos relation upon deletion of a video.
* Server: Modify BlacklistedVideos schema to add Pod id information.
* Server: Moving insertion of a blacklisted video from the `afterDestroy` hook into the process of deletion of a video.
To avoid inserting a video when it is removed on its origin pod.
When a video is removed on its origin pod, the `afterDestroy` hook is fire, but no request is made on the delete('/:videoId') interface.
Hence, we insert into `BlacklistedVideos` only on request on delete('/:videoId') (if requirements for insertion are met).
* Server: Add removeVideoFromBlacklist hook on deletion of a video.
We are going to proceed in another way :).
We will add a new route : /:videoId/blacklist to blacklist a video.
We do not blacklist a video upon its deletion now (to distinguish a video blacklist from a regular video delete)
When we blacklist a video, the video remains in the DB, so we don't have any concern about its update. It just doesn't appear in the video list.
When we remove a video, we then have to remove it from the blacklist too.
We could also remove a video from the blacklist to 'unremove' it and make it appear again in the video list (will be another feature).
* Server: Add handler for new route post(/:videoId/blacklist)
* Client: Add isBlacklistable method
* Client: Update isRemovableBy method.
* Client: Move 'Delete video' feature from the video-list to the video-watch module.
* Server: Exclude blacklisted videos from the video list
* Server: Use findAll() in BlacklistedVideos.list() method
* Server: Fix addVideoToBlacklist function.
* Client: Add blacklist feature.
* Server: Use JavaScript Standard Style.
* Server: In checkUserCanDeleteVideo, move the callback call inside the db callback function
* Server: Modify BlacklistVideo relation
* Server: Modifiy Videos methods.
* Server: Add checkVideoIsBlacklistable method
* Server: Rewrite addVideoToBlacklist method
* Server: Fix checkVideoIsBlacklistable method
* Server: Add return to addVideoToBlacklist method
this.by = Video.createByString(hash.author, hash.podHost);
}
- isRemovableBy(user: User) {
- return this.isLocal === true && user && this.author === user.username;
+ isRemovableBy(user) {
+ return user && this.isLocal === true && (this.author === user.username || user.isAdmin() === true);
+ }
+
+ isBlackistableBy(user) {
+ return user && user.isAdmin() === true && this.isLocal === false;
}
isVideoNSFWForUser(user: User) {
.catch((res) => this.restExtractor.handleError(res));
}
+ blacklistVideo(id: string) {
+ return this.authHttp.post(VideoService.BASE_VIDEO_URL + id + '/blacklist', {})
+ .map(this.restExtractor.extractDataBool)
+ .catch((res) => this.restExtractor.handleError(res));
+ }
+
private setVideoRate(id: string, rateType: RateType) {
const url = VideoService.BASE_VIDEO_URL + id + '/rate';
const body = {
this.navigateToNewParams();
}
- onRemoved(video: Video) {
- this.notificationsService.success('Success', `Video ${video.name} deleted.`);
- this.getVideos();
- }
-
onSort(sort: SortField) {
this.sort = sort;
<span class="video-miniature-duration">{{ video.duration }}</span>
</a>
- <span
- *ngIf="displayRemoveIcon()" (click)="removeVideo(video.id)"
- class="video-miniature-remove glyphicon glyphicon-remove"
- ></span>
<div class="video-miniature-informations">
<span class="video-miniature-name-tags">
}
}
- .video-miniature-remove {
- display: inline-block;
- position: absolute;
- left: 16px;
- background-color: rgba(0, 0, 0, 0.8);
- color: rgba(255, 255, 255, 0.8);
- padding: 2px;
- cursor: pointer;
-
- &:hover {
- color: rgba(255, 255, 255, 0.9);
- }
- }
-
.video-miniature-informations {
width: 200px;
})
export class VideoMiniatureComponent {
- @Output() removed = new EventEmitter<any>();
-
@Input() currentSort: SortField;
@Input() user: User;
@Input() video: Video;
private videoService: VideoService
) {}
- displayRemoveIcon() {
- return this.hovering && this.video.isRemovableBy(this.user);
- }
-
getVideoName() {
if (this.isVideoNSFWForThisUser())
return 'NSFW';
this.hovering = true;
}
- removeVideo(id: string) {
- this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe(
- res => {
- if (res === false) return;
-
- this.videoService.removeVideo(id).subscribe(
- status => this.removed.emit(true),
-
- error => this.notificationsService.error('Error', error.text)
- );
- }
- );
- }
-
isVideoNSFWForThisUser() {
return this.video.isVideoNSFWForUser(this.user);
}
<span class="glyphicon glyphicon-alert"></span> Report
</a>
</li>
+
+ <li *ngIf="isVideoRemovable()" role="menuitem">
+ <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)">
+ <span class="glyphicon glyphicon-remove"></span> Delete
+ </a>
+ </li>
+
+ <li *ngIf="isVideoBlacklistable()" role="menuitem">
+ <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
+ <span class="glyphicon glyphicon-eye-close"></span> Blacklist
+ </a>
+ </li>
</ul>
</div>
</div>
);
}
+ removeVideo(event: Event) {
+ event.preventDefault();
+ this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe(
+ res => {
+ if (res === false) return;
+
+ this.videoService.removeVideo(this.video.id)
+ .subscribe(
+ status => {
+ this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
+ // Go back to the video-list.
+ this.router.navigate(['/videos/list'])
+ },
+
+ error => this.notificationsService.error('Error', error.text)
+ );
+ }
+ );
+ }
+
+ blacklistVideo(event: Event) {
+ event.preventDefault()
+ this.confirmService.confirm('Do you really want to blacklist this video ?', 'Blacklist').subscribe(
+ res => {
+ if (res === false) return;
+
+ this.videoService.blacklistVideo(this.video.id)
+ .subscribe(
+ status => {
+ this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
+ this.router.navigate(['/videos/list'])
+ },
+
+ error => this.notificationsService.error('Error', error.text)
+ )
+ }
+ )
+ }
+
showReportModal(event: Event) {
event.preventDefault();
this.videoReportModal.show();
this.authService.getUser().username === this.video.author;
}
+ isVideoRemovable() {
+ return this.video.isRemovableBy(this.authService.getUser());
+ }
+
+ isVideoBlacklistable() {
+ return this.video.isBlackistableBy(this.authService.getUser());
+ }
+
private checkUserRating() {
// Unlogged users do not have ratings
if (this.isUserLoggedIn() === false) return;
validatorsVideos.videosGet,
getVideo
)
+
router.delete('/:id',
oAuth.authenticate,
validatorsVideos.videosRemove,
removeVideo
)
+
router.get('/search/:value',
validatorsVideos.videosSearch,
validatorsPagination.pagination,
searchVideos
)
+router.post('/:id/blacklist',
+ oAuth.authenticate,
+ admin.ensureIsAdmin,
+ validatorsVideos.videosBlacklist,
+ addVideoToBlacklist
+)
+
// ---------------------------------------------------------------------------
module.exports = router
return finalCallback(null)
})
}
+
+function addVideoToBlacklist (req, res, next) {
+ const videoInstance = res.locals.video
+
+ db.BlacklistedVideo.create({
+ videoId: videoInstance.id
+ })
+ .asCallback(function (err) {
+ if (err) {
+ logger.error('Errors when blacklisting video ', { error: err })
+ return next(err)
+ }
+
+ return res.type('json').status(204).end()
+ })
+}
videoAbuseReport,
- videoRate
+ videoRate,
+
+ videosBlacklist
}
function videosAdd (req, res, next) {
checkVideoExists(req.params.id, res, function () {
// We need to make additional checks
- if (res.locals.video.isOwned() === false) {
- return res.status(403).send('Cannot remove video of another pod')
- }
-
- if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) {
- return res.status(403).send('Cannot remove video of another user')
- }
-
- next()
+ // Check if the user who did the request is able to delete the video
+ checkUserCanDeleteVideo(res.locals.oauth.token.User.id, res, function () {
+ next()
+ })
})
})
}
callback()
})
}
+
+function checkUserCanDeleteVideo (userId, res, callback) {
+ // Retrieve the user who did the request
+ db.User.loadById(userId, function (err, user) {
+ if (err) {
+ logger.error('Error in video request validator.', { error: err })
+ return res.sendStatus(500)
+ }
+
+ // Check if the user can delete the video
+ // The user can delete it if s/he an admin
+ // Or if s/he is the video's author
+ if (user.isAdmin() === false) {
+ if (res.locals.video.isOwned() === false) {
+ return res.status(403).send('Cannot remove video of another pod')
+ }
+
+ if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) {
+ return res.status(403).send('Cannot remove video of another user')
+ }
+ }
+
+ // If we reach this comment, we can delete the video
+ callback()
+ })
+}
+
+function checkVideoIsBlacklistable (req, res, callback) {
+ if (res.locals.video.isOwned() === true) {
+ return res.status(403).send('Cannot blacklist a local video')
+ }
+
+ callback()
+}
+
+function videosBlacklist (req, res, next) {
+ req.checkParams('id', 'Should have a valid id').notEmpty().isUUID(4)
+
+ logger.debug('Checking videosBlacklist parameters', { parameters: req.params })
+
+ checkErrors(req, res, function () {
+ checkVideoExists(req.params.id, res, function() {
+ checkVideoIsBlacklistable(req, res, next)
+ })
+ })
+}
},
instanceMethods: {
isPasswordMatch,
- toFormatedJSON
+ toFormatedJSON,
+ isAdmin
},
hooks: {
beforeCreate: beforeCreateOrUpdate,
createdAt: this.createdAt
}
}
+
+function isAdmin () {
+ return this.role === constants.USER_ROLES.ADMIN
+}
+
// ------------------------------ STATICS ------------------------------
function associate (models) {
--- /dev/null
+'use strict'
+
+const modelUtils = require('./utils')
+
+// ---------------------------------------------------------------------------
+
+module.exports = function (sequelize, DataTypes) {
+ const BlacklistedVideo = sequelize.define('BlacklistedVideo',
+ {},
+ {
+ indexes: [
+ {
+ fields: [ 'videoId' ],
+ unique: true
+ }
+ ],
+ classMethods: {
+ associate,
+
+ countTotal,
+ list,
+ listForApi,
+ loadById,
+ loadByVideoId
+ },
+ instanceMethods: {
+ toFormatedJSON
+ },
+ hooks: {}
+ }
+ )
+
+ return BlacklistedVideo
+}
+
+// ------------------------------ METHODS ------------------------------
+
+function toFormatedJSON () {
+ return {
+ id: this.id,
+ videoId: this.videoId,
+ createdAt: this.createdAt
+ }
+}
+
+// ------------------------------ STATICS ------------------------------
+
+function associate (models) {
+ this.belongsTo(models.Video, {
+ foreignKey: 'videoId',
+ onDelete: 'cascade'
+ })
+}
+
+function countTotal (callback) {
+ return this.count().asCallback(callback)
+}
+
+function list (callback) {
+ return this.findAll().asCallback(callback)
+}
+
+function listForApi (start, count, sort, callback) {
+ const query = {
+ offset: start,
+ limit: count,
+ order: [ modelUtils.getSort(sort) ]
+ }
+
+ return this.findAndCountAll(query).asCallback(function (err, result) {
+ if (err) return callback(err)
+
+ return callback(null, result.rows, result.count)
+ })
+}
+
+function loadById (id, callback) {
+ return this.findById(id).asCallback(callback)
+}
+
+function loadByVideoId (id, callback) {
+ const query = {
+ where: {
+ videoId: id
+ }
+ }
+
+ return this.find(query).asCallback(callback)
+}
const friends = require('../lib/friends')
const modelUtils = require('./utils')
const customVideosValidators = require('../helpers/custom-validators').videos
+const db = require('../initializers/database')
// ---------------------------------------------------------------------------
isOwned,
toFormatedJSON,
toAddRemoteJSON,
- toUpdateRemoteJSON
+ toUpdateRemoteJSON,
+ removeFromBlacklist
},
hooks: {
beforeValidate,
}
function listForApi (start, count, sort, callback) {
+ // Exclude Blakclisted videos from the list
const query = {
offset: start,
limit: count,
},
this.sequelize.models.Tag
- ]
+ ],
+ where: {
+ id: { $notIn: this.sequelize.literal(
+ '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
+ )}
+ }
}
return this.findAndCountAll(query).asCallback(function (err, result) {
}
const query = {
- where: {},
+ where: {
+ id: { $notIn: this.sequelize.literal(
+ '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
+ )}
+ },
offset: start,
limit: count,
distinct: true, // For the count, a video can have many tags
query.where.infoHash = infoHash
} else if (field === 'tags') {
const escapedValue = this.sequelize.escape('%' + value + '%')
- query.where = {
- id: {
- $in: this.sequelize.literal(
- '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')'
- )
- }
- }
+ query.where.id.$in = this.sequelize.literal(
+ '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')'
+ )
} else if (field === 'host') {
// FIXME: Include our pod? (not stored in the database)
podInclude.where = {
})
.thumbnail(options)
}
+
+function removeFromBlacklist (video, callback) {
+ // Find the blacklisted video
+ db.BlacklistedVideo.loadByVideoId(video.id, function (err, video) {
+ // If an error occured, stop here
+ if (err) {
+ logger.error('Error when fetching video from blacklist.', { error: err })
+
+ return callback(err)
+ }
+
+ // If we found the video, remove it from the blacklist
+ if (video) {
+ video.destroy().asCallback(callback)
+ } else {
+ // If haven't found it, simply ignore it and do nothing
+ return callback()
+ }
+ })
+}