From 0883b3245bf0deb9106c4041e9afbd3521b79280 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 19 Apr 2018 11:01:34 +0200 Subject: Add ability to choose what policy we have for NSFW videos There is a global instance setting and a per user setting --- server/controllers/api/config.ts | 3 + server/controllers/api/users.ts | 15 +- server/controllers/api/videos/index.ts | 38 +++- server/controllers/feeds.ts | 9 +- server/helpers/custom-validators/users.ts | 10 +- server/initializers/checker.ts | 11 +- server/initializers/constants.ts | 11 +- server/initializers/installer.ts | 1 + .../migrations/0205-user-nsfw-policy.ts | 46 +++++ server/middlewares/oauth.ts | 10 ++ server/middlewares/validators/config.ts | 3 +- server/middlewares/validators/users.ts | 4 +- server/models/account/user.ts | 14 +- server/models/video/video.ts | 24 ++- server/tests/api/check-params/config.ts | 19 +- server/tests/api/check-params/users.ts | 6 +- server/tests/api/server/config.ts | 5 + server/tests/api/users/users.ts | 26 +-- server/tests/api/videos/video-nsfw.ts | 197 +++++++++++++++++++++ server/tests/utils/users/users.ts | 5 +- server/tests/utils/videos/videos.ts | 26 +++ 21 files changed, 425 insertions(+), 58 deletions(-) create mode 100644 server/initializers/migrations/0205-user-nsfw-policy.ts create mode 100644 server/tests/api/videos/video-nsfw.ts (limited to 'server') diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 88f047adc..e47b71f44 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -46,6 +46,7 @@ async function getConfig (req: express.Request, res: express.Response, next: exp name: CONFIG.INSTANCE.NAME, shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, + defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, customizations: { javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT, css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS @@ -128,6 +129,7 @@ async function updateCustomConfig (req: express.Request, res: express.Response, toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription + toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy await writeFilePromise(CONFIG.CUSTOM_FILE, JSON.stringify(toUpdateJSON, undefined, 2)) @@ -153,6 +155,7 @@ function customConfig (): CustomConfig { description: CONFIG.INSTANCE.DESCRIPTION, terms: CONFIG.INSTANCE.TERMS, defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, + defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, customizations: { css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS, javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 56cbf9448..6540adb1c 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -42,6 +42,7 @@ import { AccountVideoRateModel } from '../../models/account/account-video-rate' import { UserModel } from '../../models/account/user' import { OAuthTokenModel } from '../../models/oauth/oauth-token' import { VideoModel } from '../../models/video/video' +import { VideoSortField } from '../../../client/src/app/shared/video/sort-field.type' const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) const loginRateLimiter = new RateLimit({ @@ -161,7 +162,13 @@ export { async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { const user = res.locals.oauth.token.User as UserModel - const resultList = await VideoModel.listAccountVideosForApi(user.Account.id ,req.query.start, req.query.count, req.query.sort) + const resultList = await VideoModel.listAccountVideosForApi( + user.Account.id, + req.query.start as number, + req.query.count as number, + req.query.sort as VideoSortField, + false // Display my NSFW videos + ) return res.json(getFormattedObjects(resultList.data, resultList.total)) } @@ -188,7 +195,7 @@ async function createUser (req: express.Request) { username: body.username, password: body.password, email: body.email, - displayNSFW: false, + nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, autoPlayVideo: true, role: body.role, videoQuota: body.videoQuota @@ -219,7 +226,7 @@ async function registerUser (req: express.Request) { username: body.username, password: body.password, email: body.email, - displayNSFW: false, + nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, autoPlayVideo: true, role: UserRole.USER, videoQuota: CONFIG.USER.VIDEO_QUOTA @@ -286,7 +293,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr if (body.password !== undefined) user.password = body.password if (body.email !== undefined) user.email = body.email - if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW + if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo await sequelizeTypescript.transaction(async t => { diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index b4cd67158..6e8601fa1 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -19,13 +19,18 @@ import { VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES } from '../../../initializers' -import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServerAndChannel } from '../../../lib/activitypub' +import { + fetchRemoteVideoDescription, + getVideoActivityPubUrl, + shareVideoByServerAndChannel +} from '../../../lib/activitypub' import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' import { JobQueue } from '../../../lib/job-queue' import { Redis } from '../../../lib/redis' import { asyncMiddleware, authenticate, + optionalAuthenticate, paginationValidator, setDefaultPagination, setDefaultSort, @@ -44,6 +49,9 @@ import { blacklistRouter } from './blacklist' import { videoChannelRouter } from './channel' import { videoCommentRouter } from './comment' import { rateVideoRouter } from './rate' +import { User } from '../../../../shared/models/users' +import { VideoFilter } from '../../../../shared/models/videos/video-query.type' +import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' const videosRouter = express.Router() @@ -81,6 +89,7 @@ videosRouter.get('/', videosSortValidator, setDefaultSort, setDefaultPagination, + optionalAuthenticate, asyncMiddleware(listVideos) ) videosRouter.get('/search', @@ -89,6 +98,7 @@ videosRouter.get('/search', videosSortValidator, setDefaultSort, setDefaultPagination, + optionalAuthenticate, asyncMiddleware(searchVideos) ) videosRouter.put('/:id', @@ -391,7 +401,13 @@ async function getVideoDescription (req: express.Request, res: express.Response) } async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { - const resultList = await VideoModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.filter) + const resultList = await VideoModel.listForApi( + req.query.start as number, + req.query.count as number, + req.query.sort as VideoSortField, + isNSFWHidden(res), + req.query.filter as VideoFilter + ) return res.json(getFormattedObjects(resultList.data, resultList.total)) } @@ -419,11 +435,21 @@ async function removeVideo (req: express.Request, res: express.Response) { async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) { const resultList = await VideoModel.searchAndPopulateAccountAndServer( - req.query.search, - req.query.start, - req.query.count, - req.query.sort + req.query.search as string, + req.query.start as number, + req.query.count as number, + req.query.sort as VideoSortField, + isNSFWHidden(res) ) return res.json(getFormattedObjects(resultList.data, resultList.total)) } + +function isNSFWHidden (res: express.Response) { + if (res.locals.oauth) { + const user: User = res.locals.oauth.token.User + if (user) return user.nsfwPolicy === 'do_not_list' + } + + return CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list' +} diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 3e384c48a..27ebecc40 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts @@ -6,6 +6,7 @@ import * as Feed from 'pfeed' import { ResultList } from '../../shared/models' import { AccountModel } from '../models/account/account' import { cacheRoute } from '../middlewares/cache' +import { VideoSortField } from '../../client/src/app/shared/video/sort-field.type' const feedsRouter = express.Router() @@ -31,20 +32,22 @@ async function generateFeed (req: express.Request, res: express.Response, next: let resultList: ResultList const account: AccountModel = res.locals.account + const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list' if (account) { resultList = await VideoModel.listAccountVideosForApi( account.id, start, FEEDS.COUNT, - req.query.sort, - true + req.query.sort as VideoSortField, + hideNSFW ) } else { resultList = await VideoModel.listForApi( start, FEEDS.COUNT, - req.query.sort, + req.query.sort as VideoSortField, + hideNSFW, req.query.filter, true ) diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index bbc7cc199..c0acb8218 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts @@ -1,9 +1,10 @@ import 'express-validator' import * as validator from 'validator' import { UserRole } from '../../../shared' -import { CONSTRAINTS_FIELDS } from '../../initializers' +import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers' import { exists, isFileValid } from './misc' +import { values } from 'lodash' const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS @@ -29,8 +30,9 @@ function isBoolean (value: any) { return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value)) } -function isUserDisplayNSFWValid (value: any) { - return isBoolean(value) +const nsfwPolicies = values(NSFW_POLICY_TYPES) +function isUserNSFWPolicyValid (value: any) { + return exists(value) && nsfwPolicies.indexOf(value) !== -1 } function isUserAutoPlayVideoValid (value: any) { @@ -56,7 +58,7 @@ export { isUserRoleValid, isUserVideoQuotaValid, isUserUsernameValid, - isUserDisplayNSFWValid, + isUserNSFWPolicyValid, isUserAutoPlayVideoValid, isUserDescriptionValid, isAvatarFile diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts index 71f303963..739f623c6 100644 --- a/server/initializers/checker.ts +++ b/server/initializers/checker.ts @@ -5,12 +5,12 @@ import { ApplicationModel } from '../models/application/application' import { OAuthClientModel } from '../models/oauth/oauth-client' // Some checks on configuration files +// Return an error message, or null if everything is okay function checkConfig () { - if (config.has('webserver.host')) { - let errorMessage = '`host` config key was renamed to `hostname` but it seems you still have a `host` key in your configuration files!' - errorMessage += ' Please ensure to rename your `host` configuration to `hostname`.' + const defaultNSFWPolicy = config.get('instance.default_nsfw_policy') - return errorMessage + if ([ 'do_not_list', 'blur', 'display' ].indexOf(defaultNSFWPolicy) === -1) { + return 'NSFW policy setting should be "do_not_list" or "blur" or "display" instead of ' + defaultNSFWPolicy } return null @@ -28,7 +28,8 @@ function checkMissedConfig () { 'log.level', 'user.video_quota', 'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads', - 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route' + 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', + 'instance.default_nsfw_policy' ] const miss: string[] = [] diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 5ee13389d..d1915586a 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -6,13 +6,14 @@ import { FollowState } from '../../shared/models/actors' import { VideoPrivacy } from '../../shared/models/videos' // Do not use barrels, remain constants as independent as possible import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' +import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' // Use a variable to reload the configuration if we need let config: IConfig = require('config') // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 200 +const LAST_MIGRATION_VERSION = 205 // --------------------------------------------------------------------------- @@ -167,6 +168,7 @@ const CONFIG = { get DESCRIPTION () { return config.get('instance.description') }, get TERMS () { return config.get('instance.terms') }, get DEFAULT_CLIENT_ROUTE () { return config.get('instance.default_client_route') }, + get DEFAULT_NSFW_POLICY () { return config.get('instance.default_nsfw_policy') }, CUSTOMIZATIONS: { get JAVASCRIPT () { return config.get('instance.customizations.javascript') }, get CSS () { return config.get('instance.customizations.css') } @@ -378,6 +380,12 @@ const BCRYPT_SALT_SIZE = 10 const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes +const NSFW_POLICY_TYPES: { [ id: string]: NSFWPolicyType } = { + DO_NOT_LIST: 'do_not_list', + BLUR: 'blur', + DISPLAY: 'display' +} + // --------------------------------------------------------------------------- // Express static paths (router) @@ -474,6 +482,7 @@ export { PRIVATE_RSA_KEY_SIZE, SORTABLE_COLUMNS, FEEDS, + NSFW_POLICY_TYPES, STATIC_MAX_AGE, STATIC_PATHS, ACTIVITY_PUB, diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index 09c6d5473..b0084b368 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts @@ -120,6 +120,7 @@ async function createOAuthAdminIfNotExist () { email, password, role, + nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, videoQuota: -1 } const user = new UserModel(userData) diff --git a/server/initializers/migrations/0205-user-nsfw-policy.ts b/server/initializers/migrations/0205-user-nsfw-policy.ts new file mode 100644 index 000000000..d0f6e8962 --- /dev/null +++ b/server/initializers/migrations/0205-user-nsfw-policy.ts @@ -0,0 +1,46 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { + + { + const data = { + type: Sequelize.ENUM('do_not_list', 'blur', 'display'), + allowNull: true, + defaultValue: null + } + await utils.queryInterface.addColumn('user', 'nsfwPolicy', data) + } + + { + const query = 'UPDATE "user" SET "nsfwPolicy" = \'do_not_list\'' + await utils.sequelize.query(query) + } + + { + const query = 'UPDATE "user" SET "nsfwPolicy" = \'display\' WHERE "displayNSFW" = true' + await utils.sequelize.query(query) + } + + { + const query = 'ALTER TABLE "user" ALTER COLUMN "nsfwPolicy" SET NOT NULL' + await utils.sequelize.query(query) + } + + { + await utils.queryInterface.removeColumn('user', 'displayNSFW') + } + +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts index 41a3fb718..a6f28dd5b 100644 --- a/server/middlewares/oauth.ts +++ b/server/middlewares/oauth.ts @@ -2,6 +2,7 @@ import * as express from 'express' import * as OAuthServer from 'express-oauth-server' import 'express-validator' import { OAUTH_LIFETIME } from '../initializers' +import { logger } from '../helpers/logger' const oAuthServer = new OAuthServer({ useErrorHandler: true, @@ -13,6 +14,8 @@ const oAuthServer = new OAuthServer({ function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { oAuthServer.authenticate()(req, res, err => { if (err) { + logger.warn('Cannot authenticate.', { err }) + return res.status(err.status) .json({ error: 'Token is invalid.', @@ -25,6 +28,12 @@ function authenticate (req: express.Request, res: express.Response, next: expres }) } +function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) { + if (req.header('authorization')) return authenticate(req, res, next) + + return next() +} + function token (req: express.Request, res: express.Response, next: express.NextFunction) { return oAuthServer.token()(req, res, err => { if (err) { @@ -44,5 +53,6 @@ function token (req: express.Request, res: express.Response, next: express.NextF export { authenticate, + optionalAuthenticate, token } diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index ee6f6efa4..f58c0676c 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts @@ -1,6 +1,6 @@ import * as express from 'express' import { body } from 'express-validator/check' -import { isUserVideoQuotaValid } from '../../helpers/custom-validators/users' +import { isUserNSFWPolicyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' import { logger } from '../../helpers/logger' import { areValidationErrors } from './utils' @@ -9,6 +9,7 @@ const customConfigUpdateValidator = [ body('instance.description').exists().withMessage('Should have a valid instance description'), body('instance.terms').exists().withMessage('Should have a valid instance terms'), body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'), + body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'), body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'), body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'), body('cache.previews.size').isInt().withMessage('Should have a valid previews size'), diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 6ea3d0b6c..5dd8caa3f 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -8,7 +8,7 @@ import { isAvatarFile, isUserAutoPlayVideoValid, isUserDescriptionValid, - isUserDisplayNSFWValid, + isUserNSFWPolicyValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, @@ -101,7 +101,7 @@ const usersUpdateMeValidator = [ body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'), body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'), body('email').optional().isEmail().withMessage('Should have a valid email attribute'), - body('displayNSFW').optional().custom(isUserDisplayNSFWValid).withMessage('Should have a valid display Not Safe For Work attribute'), + body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), (req: express.Request, res: express.Response, next: express.NextFunction) => { diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 8afd246b2..56af2f30a 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -21,7 +21,7 @@ import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' import { User, UserRole } from '../../../shared/models/users' import { isUserAutoPlayVideoValid, - isUserDisplayNSFWValid, + isUserNSFWPolicyValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, @@ -32,6 +32,9 @@ import { OAuthTokenModel } from '../oauth/oauth-token' import { getSort, throwIfNotValid } from '../utils' import { VideoChannelModel } from '../video/video-channel' import { AccountModel } from './account' +import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' +import { values } from 'lodash' +import { NSFW_POLICY_TYPES } from '../../initializers' @DefaultScope({ include: [ @@ -83,10 +86,9 @@ export class UserModel extends Model { email: string @AllowNull(false) - @Default(false) - @Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean')) - @Column - displayNSFW: boolean + @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy')) + @Column(DataType.ENUM(values(NSFW_POLICY_TYPES))) + nsfwPolicy: NSFWPolicyType @AllowNull(false) @Default(true) @@ -265,7 +267,7 @@ export class UserModel extends Model { id: this.id, username: this.username, email: this.email, - displayNSFW: this.displayNSFW, + nsfwPolicy: this.nsfwPolicy, autoPlayVideo: this.autoPlayVideo, role: this.role, roleLabel: USER_ROLE_LABELS[ this.role ], diff --git a/server/models/video/video.ts b/server/models/video/video.ts index a7923b477..2e66f9aa7 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -95,7 +95,7 @@ enum ScopeNames { } @Scopes({ - [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, filter?: VideoFilter, withFiles?: boolean) => { + [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, hideNSFW: boolean, filter?: VideoFilter, withFiles?: boolean) => { const query: IFindOptions = { where: { id: { @@ -161,6 +161,11 @@ enum ScopeNames { }) } + // Hide nsfw videos? + if (hideNSFW === true) { + query.where['nsfw'] = false + } + return query }, [ScopeNames.WITH_ACCOUNT_DETAILS]: { @@ -640,7 +645,7 @@ export class VideoModel extends Model { }) } - static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) { + static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) { const query: IFindOptions = { offset: start, limit: count, @@ -669,6 +674,12 @@ export class VideoModel extends Model { }) } + if (hideNSFW === true) { + query.where = { + nsfw: false + } + } + return VideoModel.findAndCountAll(query).then(({ rows, count }) => { return { data: rows, @@ -677,7 +688,7 @@ export class VideoModel extends Model { }) } - static async listForApi (start: number, count: number, sort: string, filter?: VideoFilter, withFiles = false) { + static async listForApi (start: number, count: number, sort: string, hideNSFW: boolean, filter?: VideoFilter, withFiles = false) { const query = { offset: start, limit: count, @@ -685,8 +696,7 @@ export class VideoModel extends Model { } const serverActor = await getServerActor() - - return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, filter, withFiles ] }) + return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW, filter, withFiles ] }) .findAndCountAll(query) .then(({ rows, count }) => { return { @@ -696,7 +706,7 @@ export class VideoModel extends Model { }) } - static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string) { + static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string, hideNSFW: boolean) { const query: IFindOptions = { offset: start, limit: count, @@ -724,7 +734,7 @@ export class VideoModel extends Model { const serverActor = await getServerActor() - return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id ] }) + return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW ] }) .findAndCountAll(query) .then(({ rows, count }) => { return { diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 3fe517fad..58b780f38 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts @@ -6,7 +6,7 @@ import { CustomConfig } from '../../../../shared/models/server/custom-config.mod import { createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo, - setAccessTokensToServers, userLogin + setAccessTokensToServers, userLogin, immutableAssign } from '../../utils' describe('Test config API validators', function () { @@ -20,6 +20,7 @@ describe('Test config API validators', function () { description: 'my super description', terms: 'my super terms', defaultClientRoute: '/videos/recently-added', + defaultNSFWPolicy: 'blur', customizations: { javascript: 'alert("coucou")', css: 'body { background-color: red; }' @@ -122,6 +123,22 @@ describe('Test config API validators', function () { }) }) + it('Should fail with a bad default NSFW policy', async function () { + const newUpdateParams = immutableAssign(updateParams, { + instance: { + defaultNSFWPolicy: 'hello' + } + }) + + await makePutBodyRequest({ + url: server.url, + path, + fields: newUpdateParams, + token: server.accessToken, + statusCodeExpected: 400 + }) + }) + it('Should success with the correct parameters', async function () { await makePutBodyRequest({ url: server.url, diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index a3e415b94..e8a6ffd19 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts @@ -231,9 +231,9 @@ describe('Test users API validators', function () { await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) }) - it('Should fail with an invalid display NSFW attribute', async function () { + it('Should fail with an invalid NSFW policy attribute', async function () { const fields = { - displayNSFW: -1 + nsfwPolicy: 'hello' } await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) @@ -266,7 +266,7 @@ describe('Test users API validators', function () { it('Should succeed with the correct params', async function () { const fields = { password: 'my super password', - displayNSFW: true, + nsfwPolicy: 'blur', autoPlayVideo: false, email: 'super_email@example.com' } diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index e17588142..3f1b1532c 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts @@ -59,6 +59,7 @@ describe('Test config', function () { expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') expect(data.instance.terms).to.equal('No terms for now.') expect(data.instance.defaultClientRoute).to.equal('/videos/trending') + expect(data.instance.defaultNSFWPolicy).to.equal('display') expect(data.instance.customizations.css).to.be.empty expect(data.instance.customizations.javascript).to.be.empty expect(data.cache.previews.size).to.equal(1) @@ -83,6 +84,7 @@ describe('Test config', function () { description: 'my super description', terms: 'my super terms', defaultClientRoute: '/videos/recently-added', + defaultNSFWPolicy: 'blur' as 'blur', customizations: { javascript: 'alert("coucou")', css: 'body { background-color: red; }' @@ -125,6 +127,7 @@ describe('Test config', function () { expect(data.instance.description).to.equal('my super description') expect(data.instance.terms).to.equal('my super terms') expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') + expect(data.instance.defaultNSFWPolicy).to.equal('blur') expect(data.instance.customizations.javascript).to.equal('alert("coucou")') expect(data.instance.customizations.css).to.equal('body { background-color: red; }') expect(data.cache.previews.size).to.equal(2) @@ -156,6 +159,7 @@ describe('Test config', function () { expect(data.instance.description).to.equal('my super description') expect(data.instance.terms).to.equal('my super terms') expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') + expect(data.instance.defaultNSFWPolicy).to.equal('blur') expect(data.instance.customizations.javascript).to.equal('alert("coucou")') expect(data.instance.customizations.css).to.equal('body { background-color: red; }') expect(data.cache.previews.size).to.equal(2) @@ -198,6 +202,7 @@ describe('Test config', function () { expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') expect(data.instance.terms).to.equal('No terms for now.') expect(data.instance.defaultClientRoute).to.equal('/videos/trending') + expect(data.instance.defaultNSFWPolicy).to.equal('display') expect(data.instance.customizations.css).to.be.empty expect(data.instance.customizations.javascript).to.be.empty expect(data.cache.previews.size).to.equal(1) diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index b6ab4f660..1192ef9e4 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts @@ -168,7 +168,7 @@ describe('Test users', function () { expect(user.username).to.equal('user_1') expect(user.email).to.equal('user_1@example.com') - expect(user.displayNSFW).to.be.false + expect(user.nsfwPolicy).to.equal('display') expect(user.videoQuota).to.equal(2 * 1024 * 1024) expect(user.roleLabel).to.equal('User') expect(user.id).to.be.a('number') @@ -215,12 +215,12 @@ describe('Test users', function () { const user = users[ 0 ] expect(user.username).to.equal('user_1') expect(user.email).to.equal('user_1@example.com') - expect(user.displayNSFW).to.be.false + expect(user.nsfwPolicy).to.equal('display') const rootUser = users[ 1 ] expect(rootUser.username).to.equal('root') expect(rootUser.email).to.equal('admin1@example.com') - expect(rootUser.displayNSFW).to.be.false + expect(user.nsfwPolicy).to.equal('display') userId = user.id }) @@ -239,7 +239,7 @@ describe('Test users', function () { expect(user.username).to.equal('root') expect(user.email).to.equal('admin1@example.com') expect(user.roleLabel).to.equal('Administrator') - expect(user.displayNSFW).to.be.false + expect(user.nsfwPolicy).to.equal('display') }) it('Should list only the first user by username desc', async function () { @@ -254,7 +254,7 @@ describe('Test users', function () { const user = users[ 0 ] expect(user.username).to.equal('user_1') expect(user.email).to.equal('user_1@example.com') - expect(user.displayNSFW).to.be.false + expect(user.nsfwPolicy).to.equal('display') }) it('Should list only the second user by createdAt desc', async function () { @@ -269,7 +269,7 @@ describe('Test users', function () { const user = users[ 0 ] expect(user.username).to.equal('user_1') expect(user.email).to.equal('user_1@example.com') - expect(user.displayNSFW).to.be.false + expect(user.nsfwPolicy).to.equal('display') }) it('Should list all the users by createdAt asc', async function () { @@ -283,11 +283,11 @@ describe('Test users', function () { expect(users[ 0 ].username).to.equal('root') expect(users[ 0 ].email).to.equal('admin1@example.com') - expect(users[ 0 ].displayNSFW).to.be.false + expect(users[ 0 ].nsfwPolicy).to.equal('display') expect(users[ 1 ].username).to.equal('user_1') expect(users[ 1 ].email).to.equal('user_1@example.com') - expect(users[ 1 ].displayNSFW).to.be.false + expect(users[ 1 ].nsfwPolicy).to.equal('display') }) it('Should update my password', async function () { @@ -305,7 +305,7 @@ describe('Test users', function () { await updateMyUser({ url: server.url, accessToken: accessTokenUser, - displayNSFW: true + nsfwPolicy: 'do_not_list' }) const res = await getMyUserInformation(server.url, accessTokenUser) @@ -313,7 +313,7 @@ describe('Test users', function () { expect(user.username).to.equal('user_1') expect(user.email).to.equal('user_1@example.com') - expect(user.displayNSFW).to.be.ok + expect(user.nsfwPolicy).to.equal('do_not_list') expect(user.videoQuota).to.equal(2 * 1024 * 1024) expect(user.id).to.be.a('number') expect(user.account.description).to.be.null @@ -344,7 +344,7 @@ describe('Test users', function () { expect(user.username).to.equal('user_1') expect(user.email).to.equal('updated@example.com') - expect(user.displayNSFW).to.be.ok + expect(user.nsfwPolicy).to.equal('do_not_list') expect(user.videoQuota).to.equal(2 * 1024 * 1024) expect(user.id).to.be.a('number') expect(user.account.description).to.be.null @@ -377,7 +377,7 @@ describe('Test users', function () { expect(user.username).to.equal('user_1') expect(user.email).to.equal('updated@example.com') - expect(user.displayNSFW).to.be.ok + expect(user.nsfwPolicy).to.equal('do_not_list') expect(user.videoQuota).to.equal(2 * 1024 * 1024) expect(user.id).to.be.a('number') expect(user.account.description).to.equal('my super description updated') @@ -398,7 +398,7 @@ describe('Test users', function () { expect(user.username).to.equal('user_1') expect(user.email).to.equal('updated2@example.com') - expect(user.displayNSFW).to.be.ok + expect(user.nsfwPolicy).to.equal('do_not_list') expect(user.videoQuota).to.equal(42) expect(user.roleLabel).to.equal('Moderator') expect(user.id).to.be.a('number') diff --git a/server/tests/api/videos/video-nsfw.ts b/server/tests/api/videos/video-nsfw.ts new file mode 100644 index 000000000..4e5ab11ce --- /dev/null +++ b/server/tests/api/videos/video-nsfw.ts @@ -0,0 +1,197 @@ +/* tslint:disable:no-unused-expression */ + +import * as chai from 'chai' +import 'mocha' +import { flushTests, getVideosList, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index' +import { userLogin } from '../../utils/users/login' +import { createUser } from '../../utils/users/users' +import { getMyVideos } from '../../utils/videos/videos' +import { + getConfig, getCustomConfig, + getMyUserInformation, + getVideosListWithToken, + runServer, + searchVideo, + searchVideoWithToken, updateCustomConfig, + updateMyUser +} from '../../utils' +import { ServerConfig } from '../../../../shared/models' +import { CustomConfig } from '../../../../shared/models/server/custom-config.model' + +const expect = chai.expect + +describe('Test video NSFW policy', function () { + let server: ServerInfo + let userAccessToken: string + let customConfig: CustomConfig + + before(async function () { + this.timeout(50000) + + await flushTests() + server = await runServer(1) + + // Get the access tokens + await setAccessTokensToServers([ server ]) + + { + const attributes = { name: 'nsfw', nsfw: true } + await uploadVideo(server.url, server.accessToken, attributes) + } + + { + const attributes = { name: 'normal', nsfw: false } + await uploadVideo(server.url, server.accessToken, attributes) + } + + { + const res = await getCustomConfig(server.url, server.accessToken) + customConfig = res.body + } + }) + + describe('Instance default NSFW policy', function () { + it('Should display NSFW videos with display default NSFW policy', async function () { + const resConfig = await getConfig(server.url) + const serverConfig: ServerConfig = resConfig.body + expect(serverConfig.instance.defaultNSFWPolicy).to.equal('display') + + for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) { + expect(res.body.total).to.equal(2) + + const videos = res.body.data + expect(videos).to.have.lengthOf(2) + expect(videos[ 0 ].name).to.equal('normal') + expect(videos[ 1 ].name).to.equal('nsfw') + } + }) + + it('Should not display NSFW videos with do_not_list default NSFW policy', async function () { + customConfig.instance.defaultNSFWPolicy = 'do_not_list' + await updateCustomConfig(server.url, server.accessToken, customConfig) + + const resConfig = await getConfig(server.url) + const serverConfig: ServerConfig = resConfig.body + expect(serverConfig.instance.defaultNSFWPolicy).to.equal('do_not_list') + + for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) { + expect(res.body.total).to.equal(1) + + const videos = res.body.data + expect(videos).to.have.lengthOf(1) + expect(videos[ 0 ].name).to.equal('normal') + } + }) + + it('Should display NSFW videos with blur default NSFW policy', async function () { + customConfig.instance.defaultNSFWPolicy = 'blur' + await updateCustomConfig(server.url, server.accessToken, customConfig) + + const resConfig = await getConfig(server.url) + const serverConfig: ServerConfig = resConfig.body + expect(serverConfig.instance.defaultNSFWPolicy).to.equal('blur') + + for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) { + expect(res.body.total).to.equal(2) + + const videos = res.body.data + expect(videos).to.have.lengthOf(2) + expect(videos[ 0 ].name).to.equal('normal') + expect(videos[ 1 ].name).to.equal('nsfw') + } + }) + }) + + describe('User NSFW policy', function () { + + it('Should create a user having the default nsfw policy', async function () { + const username = 'user1' + const password = 'my super password' + await createUser(server.url, server.accessToken, username, password) + + userAccessToken = await userLogin(server, { username, password }) + + const res = await getMyUserInformation(server.url, userAccessToken) + const user = res.body + + expect(user.nsfwPolicy).to.equal('blur') + }) + + it('Should display NSFW videos with blur user NSFW policy', async function () { + const results = [ + await getVideosListWithToken(server.url, userAccessToken), + await searchVideoWithToken(server.url, 'n', userAccessToken) + ] + + for (const res of results) { + expect(res.body.total).to.equal(2) + + const videos = res.body.data + expect(videos).to.have.lengthOf(2) + expect(videos[ 0 ].name).to.equal('normal') + expect(videos[ 1 ].name).to.equal('nsfw') + } + }) + + it('Should display NSFW videos with display user NSFW policy', async function () { + await updateMyUser({ + url: server.url, + accessToken: server.accessToken, + nsfwPolicy: 'display' + }) + + const results = [ + await getVideosListWithToken(server.url, server.accessToken), + await searchVideoWithToken(server.url, 'n', server.accessToken) + ] + + for (const res of results) { + expect(res.body.total).to.equal(2) + + const videos = res.body.data + expect(videos).to.have.lengthOf(2) + expect(videos[ 0 ].name).to.equal('normal') + expect(videos[ 1 ].name).to.equal('nsfw') + } + }) + + it('Should not display NSFW videos with do_not_list user NSFW policy', async function () { + await updateMyUser({ + url: server.url, + accessToken: server.accessToken, + nsfwPolicy: 'do_not_list' + }) + + const results = [ + await getVideosListWithToken(server.url, server.accessToken), + await searchVideoWithToken(server.url, 'n', server.accessToken) + ] + for (const res of results) { + expect(res.body.total).to.equal(1) + + const videos = res.body.data + expect(videos).to.have.lengthOf(1) + expect(videos[ 0 ].name).to.equal('normal') + } + }) + + it('Should be able to see my NSFW videos even with do_not_list user NSFW policy', async function () { + const res = await getMyVideos(server.url, server.accessToken, 0, 5) + expect(res.body.total).to.equal(2) + + const videos = res.body.data + expect(videos).to.have.lengthOf(2) + expect(videos[ 0 ].name).to.equal('normal') + expect(videos[ 1 ].name).to.equal('nsfw') + }) + }) + + after(async function () { + killallServers([ server ]) + + // Keep the logs if the test failed + if (this['ok']) { + await flushTests() + } + }) +}) diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts index daf731a14..fc6b26c50 100644 --- a/server/tests/utils/users/users.ts +++ b/server/tests/utils/users/users.ts @@ -3,6 +3,7 @@ import * as request from 'supertest' import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../' import { UserRole } from '../../../../shared/index' +import { NSFWPolicyType } from '../../../../shared/models/videos/nsfw-policy.type' function createUser ( url: string, @@ -128,7 +129,7 @@ function updateMyUser (options: { url: string accessToken: string, newPassword?: string, - displayNSFW?: boolean, + nsfwPolicy?: NSFWPolicyType, email?: string, autoPlayVideo?: boolean description?: string @@ -137,7 +138,7 @@ function updateMyUser (options: { const toSend = {} if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword - if (options.displayNSFW !== undefined && options.displayNSFW !== null) toSend['displayNSFW'] = options.displayNSFW + if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend['nsfwPolicy'] = options.nsfwPolicy if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo if (options.email !== undefined && options.email !== null) toSend['email'] = options.email if (options.description !== undefined && options.description !== null) toSend['description'] = options.description diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts index 01e7fa5a1..df9071c29 100644 --- a/server/tests/utils/videos/videos.ts +++ b/server/tests/utils/videos/videos.ts @@ -128,6 +128,18 @@ function getVideosList (url: string) { .expect('Content-Type', /json/) } +function getVideosListWithToken (url: string, token: string) { + const path = '/api/v1/videos' + + return request(url) + .get(path) + .set('Authorization', 'Bearer ' + token) + .query({ sort: 'name' }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) +} + function getLocalVideos (url: string) { const path = '/api/v1/videos' @@ -202,6 +214,18 @@ function searchVideo (url: string, search: string) { .expect('Content-Type', /json/) } +function searchVideoWithToken (url: string, search: string, token: string) { + const path = '/api/v1/videos' + const req = request(url) + .get(path + '/search') + .set('Authorization', 'Bearer ' + token) + .query({ search }) + .set('Accept', 'application/json') + + return req.expect(200) + .expect('Content-Type', /json/) +} + function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) { const path = '/api/v1/videos' @@ -490,6 +514,7 @@ export { getVideoPrivacies, getVideoLanguages, getMyVideos, + searchVideoWithToken, getVideo, getVideoWithToken, getVideosList, @@ -499,6 +524,7 @@ export { searchVideo, searchVideoWithPagination, searchVideoWithSort, + getVideosListWithToken, uploadVideo, updateVideo, rateVideo, -- cgit v1.2.3