languageOneOf: req.query.languageOneOf,
tagsOneOf: req.query.tagsOneOf,
tagsAllOf: req.query.tagsAllOf,
+ filter: req.query.filter,
nsfw: buildNSFWFilter(res, req.query.nsfw),
withFiles: false,
- accountId: account.id
+ accountId: account.id,
+ userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
const options = Object.assign(query, {
includeLocalVideos: true,
nsfw: buildNSFWFilter(res, query.nsfw),
+ filter: query.filter,
userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
})
const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
languageOneOf: req.query.languageOneOf,
tagsOneOf: req.query.tagsOneOf,
tagsAllOf: req.query.tagsAllOf,
+ filter: req.query.filter,
nsfw: buildNSFWFilter(res, req.query.nsfw),
withFiles: false,
- videoChannelId: videoChannelInstance.id
+ videoChannelId: videoChannelInstance.id,
+ userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
import * as express from 'express'
import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants'
import { THUMBNAILS_SIZE } from '../initializers'
-import { asyncMiddleware, setDefaultSort, videoCommentsFeedsValidator, videoFeedsValidator, videosSortValidator } from '../middlewares'
+import {
+ asyncMiddleware,
+ commonVideosFiltersValidator,
+ setDefaultSort,
+ videoCommentsFeedsValidator,
+ videoFeedsValidator,
+ videosSortValidator
+} from '../middlewares'
import { VideoModel } from '../models/video/video'
import * as Feed from 'pfeed'
import { AccountModel } from '../models/account/account'
videosSortValidator,
setDefaultSort,
asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)),
+ commonVideosFiltersValidator,
asyncMiddleware(videoFeedsValidator),
asyncMiddleware(generateVideoFeed)
)
import { values } from 'lodash'
import 'multer'
import * as validator from 'validator'
-import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared'
+import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
import {
CONSTRAINTS_FIELDS,
VIDEO_CATEGORIES,
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
+function isVideoFilterValid (filter: VideoFilter) {
+ return filter === 'local' || filter === 'all-local'
+}
+
function isVideoCategoryValid (value: any) {
return value === null || VIDEO_CATEGORIES[ value ] !== undefined
}
isVideoExist,
isVideoImage,
isVideoChannelOfAccountExist,
- isVideoSupportValid
+ isVideoSupportValid,
+ isVideoFilterValid
}
import * as multer from 'multer'
import { CONFIG, REMOTE_SCHEME } from '../initializers'
import { logger } from './logger'
-import { User } from '../../shared/models/users'
import { deleteFileAsync, generateRandomString } from './utils'
import { extname } from 'path'
import { isArray } from './custom-validators/misc'
}
function isUserAbleToSearchRemoteURI (res: express.Response) {
- const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined
+ const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
(CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)
import { areValidationErrors } from './utils'
import { logger } from '../../helpers/logger'
import { query } from 'express-validator/check'
-import { isNumberArray, isStringArray, isNSFWQueryValid } from '../../helpers/custom-validators/search'
-import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc'
+import { isDateValid } from '../../helpers/custom-validators/misc'
const videosSearchValidator = [
query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
}
]
-const commonVideosFiltersValidator = [
- query('categoryOneOf')
- .optional()
- .customSanitizer(toArray)
- .custom(isNumberArray).withMessage('Should have a valid one of category array'),
- query('licenceOneOf')
- .optional()
- .customSanitizer(toArray)
- .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
- query('languageOneOf')
- .optional()
- .customSanitizer(toArray)
- .custom(isStringArray).withMessage('Should have a valid one of language array'),
- query('tagsOneOf')
- .optional()
- .customSanitizer(toArray)
- .custom(isStringArray).withMessage('Should have a valid one of tags array'),
- query('tagsAllOf')
- .optional()
- .customSanitizer(toArray)
- .custom(isStringArray).withMessage('Should have a valid all of tags array'),
- query('nsfw')
- .optional()
- .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
-
- (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking commons video filters query', { parameters: req.query })
-
- if (areValidationErrors(req, res)) return
-
- return next()
- }
-]
-
// ---------------------------------------------------------------------------
export {
- commonVideosFiltersValidator,
videoChannelsSearchValidator,
videosSearchValidator
}
import * as express from 'express'
import 'express-validator'
-import { body, param, ValidationChain } from 'express-validator/check'
+import { body, param, query, ValidationChain } from 'express-validator/check'
import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
import {
isBooleanValid,
isIdOrUUIDValid,
isIdValid,
isUUIDValid,
+ toArray,
toIntOrNull,
toValueOrNull
} from '../../../helpers/custom-validators/misc'
isVideoDescriptionValid,
isVideoExist,
isVideoFile,
+ isVideoFilterValid,
isVideoImage,
isVideoLanguageValid,
isVideoLicenceValid,
import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
import { AccountModel } from '../../../models/account/account'
import { VideoFetchType } from '../../../helpers/video'
+import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
const videosAddValidator = getCommonVideoAttributes().concat([
body('videofile')
] as (ValidationChain | express.Handler)[]
}
+const commonVideosFiltersValidator = [
+ query('categoryOneOf')
+ .optional()
+ .customSanitizer(toArray)
+ .custom(isNumberArray).withMessage('Should have a valid one of category array'),
+ query('licenceOneOf')
+ .optional()
+ .customSanitizer(toArray)
+ .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
+ query('languageOneOf')
+ .optional()
+ .customSanitizer(toArray)
+ .custom(isStringArray).withMessage('Should have a valid one of language array'),
+ query('tagsOneOf')
+ .optional()
+ .customSanitizer(toArray)
+ .custom(isStringArray).withMessage('Should have a valid one of tags array'),
+ query('tagsAllOf')
+ .optional()
+ .customSanitizer(toArray)
+ .custom(isStringArray).withMessage('Should have a valid all of tags array'),
+ query('nsfw')
+ .optional()
+ .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
+ query('filter')
+ .optional()
+ .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking commons video filters query', { parameters: req.query })
+
+ if (areValidationErrors(req, res)) return
+
+ const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
+ if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
+ res.status(401)
+ .json({ error: 'You are not allowed to see all local videos.' })
+
+ return
+ }
+
+ return next()
+ }
+]
+
// ---------------------------------------------------------------------------
export {
videosTerminateChangeOwnershipValidator,
videosAcceptChangeOwnershipValidator,
- getCommonVideoAttributes
+ getCommonVideoAttributes,
+
+ commonVideosFiltersValidator
}
// ---------------------------------------------------------------------------
)
}
]
- },
+ }
+ },
+ include: []
+ }
+
+ // Only list public/published videos
+ if (!options.filter || options.filter !== 'all-local') {
+ const privacyWhere = {
// Always list public videos
privacy: VideoPrivacy.PUBLIC,
// Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
}
}
]
- },
- include: []
+ }
+
+ Object.assign(query.where, privacyWhere)
}
if (options.filter || options.accountId || options.videoChannelId) {
trendingDays?: number,
userId?: number
}, countVideos = true) {
+ if (options.filter && options.filter === 'all-local' && !options.userId) {
+ throw new Error('Try to filter all-local but no userId is provided')
+ }
+
const query: IFindOptions<VideoModel> = {
offset: options.start,
limit: options.count,
tagsAllOf?: string[]
durationMin?: number // seconds
durationMax?: number // seconds
- userId?: number
+ userId?: number,
+ filter?: VideoFilter
}) {
const whereAnd = []
languageOneOf: options.languageOneOf,
tagsOneOf: options.tagsOneOf,
tagsAllOf: options.tagsAllOf,
- userId: options.userId
+ userId: options.userId,
+ filter: options.filter
}
return VideoModel.getAvailableForApi(query, queryOptions)
}
private static buildActorWhereWithFilter (filter?: VideoFilter) {
- if (filter && filter === 'local') {
+ if (filter && (filter === 'local' || filter === 'all-local')) {
return {
serverId: null
}
import './video-comments'
import './video-imports'
import './videos'
+import './videos-filter'
import './videos-history'
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import {
+ createUser,
+ flushTests,
+ killallServers,
+ makeGetRequest,
+ runServer,
+ ServerInfo,
+ setAccessTokensToServers,
+ userLogin
+} from '../../utils'
+import { UserRole } from '../../../../shared/models/users'
+
+const expect = chai.expect
+
+async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) {
+ const paths = [
+ '/api/v1/video-channels/root_channel/videos',
+ '/api/v1/accounts/root/videos',
+ '/api/v1/videos',
+ '/api/v1/search/videos'
+ ]
+
+ for (const path of paths) {
+ await makeGetRequest({
+ url: server.url,
+ path,
+ token,
+ query: {
+ filter
+ },
+ statusCodeExpected
+ })
+ }
+}
+
+describe('Test videos filters', function () {
+ let server: ServerInfo
+ let userAccessToken: string
+ let moderatorAccessToken: string
+
+ // ---------------------------------------------------------------
+
+ before(async function () {
+ this.timeout(30000)
+
+ await flushTests()
+
+ server = await runServer(1)
+
+ await setAccessTokensToServers([ server ])
+
+ const user = { username: 'user1', password: 'my super password' }
+ await createUser(server.url, server.accessToken, user.username, user.password)
+ userAccessToken = await userLogin(server, user)
+
+ const moderator = { username: 'moderator', password: 'my super password' }
+ await createUser(
+ server.url,
+ server.accessToken,
+ moderator.username,
+ moderator.password,
+ undefined,
+ undefined,
+ UserRole.MODERATOR
+ )
+ moderatorAccessToken = await userLogin(server, moderator)
+ })
+
+ describe('When setting a video filter', function () {
+
+ it('Should fail with a bad filter', async function () {
+ await testEndpoints(server, server.accessToken, 'bad-filter', 400)
+ })
+
+ it('Should succeed with a good filter', async function () {
+ await testEndpoints(server, server.accessToken,'local', 200)
+ })
+
+ it('Should fail to list all-local with a simple user', async function () {
+ await testEndpoints(server, userAccessToken, 'all-local', 401)
+ })
+
+ it('Should succeed to list all-local with a moderator', async function () {
+ await testEndpoints(server, moderatorAccessToken, 'all-local', 200)
+ })
+
+ it('Should succeed to list all-local with an admin', async function () {
+ await testEndpoints(server, server.accessToken, 'all-local', 200)
+ })
+
+ // Because we cannot authenticate the user on the RSS endpoint
+ it('Should fail on the feeds endpoint with the all-local filter', async function () {
+ await makeGetRequest({
+ url: server.url,
+ path: '/feeds/videos.json',
+ statusCodeExpected: 401,
+ query: {
+ filter: 'all-local'
+ }
+ })
+ })
+
+ it('Should succed on the feeds endpoint with the local filter', async function () {
+ await makeGetRequest({
+ url: server.url,
+ path: '/feeds/videos.json',
+ statusCodeExpected: 200,
+ query: {
+ filter: 'local'
+ }
+ })
+ })
+ })
+
+ after(async function () {
+ killallServers([ server ])
+
+ // Keep the logs if the test failed
+ if (this['ok']) {
+ await flushTests()
+ }
+ })
+})
import './video-privacy'
import './video-schedule-update'
import './video-transcoder'
+import './videos-filter'
import './videos-history'
import './videos-overview'
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import {
+ createUser,
+ doubleFollow,
+ flushAndRunMultipleServers,
+ flushTests,
+ killallServers,
+ makeGetRequest,
+ ServerInfo,
+ setAccessTokensToServers,
+ uploadVideo,
+ userLogin
+} from '../../utils'
+import { Video, VideoPrivacy } from '../../../../shared/models/videos'
+import { UserRole } from '../../../../shared/models/users'
+
+const expect = chai.expect
+
+async function getVideosNames (server: ServerInfo, token: string, filter: string, statusCodeExpected = 200) {
+ const paths = [
+ '/api/v1/video-channels/root_channel/videos',
+ '/api/v1/accounts/root/videos',
+ '/api/v1/videos',
+ '/api/v1/search/videos'
+ ]
+
+ const videosResults: Video[][] = []
+
+ for (const path of paths) {
+ const res = await makeGetRequest({
+ url: server.url,
+ path,
+ token,
+ query: {
+ sort: 'createdAt',
+ filter
+ },
+ statusCodeExpected
+ })
+
+ videosResults.push(res.body.data.map(v => v.name))
+ }
+
+ return videosResults
+}
+
+describe('Test videos filter validator', function () {
+ let servers: ServerInfo[]
+
+ // ---------------------------------------------------------------
+
+ before(async function () {
+ this.timeout(120000)
+
+ await flushTests()
+
+ servers = await flushAndRunMultipleServers(2)
+
+ await setAccessTokensToServers(servers)
+
+ for (const server of servers) {
+ const moderator = { username: 'moderator', password: 'my super password' }
+ await createUser(
+ server.url,
+ server.accessToken,
+ moderator.username,
+ moderator.password,
+ undefined,
+ undefined,
+ UserRole.MODERATOR
+ )
+ server['moderatorAccessToken'] = await userLogin(server, moderator)
+
+ await uploadVideo(server.url, server.accessToken, { name: 'public ' + server.serverNumber })
+
+ {
+ const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED }
+ await uploadVideo(server.url, server.accessToken, attributes)
+ }
+
+ {
+ const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE }
+ await uploadVideo(server.url, server.accessToken, attributes)
+ }
+ }
+
+ await doubleFollow(servers[0], servers[1])
+ })
+
+ describe('Check videos filter', function () {
+
+ it('Should display local videos', async function () {
+ for (const server of servers) {
+ const namesResults = await getVideosNames(server, server.accessToken, 'local')
+ for (const names of namesResults) {
+ expect(names).to.have.lengthOf(1)
+ expect(names[ 0 ]).to.equal('public ' + server.serverNumber)
+ }
+ }
+ })
+
+ it('Should display all local videos by the admin or the moderator', async function () {
+ for (const server of servers) {
+ for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
+
+ const namesResults = await getVideosNames(server, token, 'all-local')
+ for (const names of namesResults) {
+ expect(names).to.have.lengthOf(3)
+
+ expect(names[ 0 ]).to.equal('public ' + server.serverNumber)
+ expect(names[ 1 ]).to.equal('unlisted ' + server.serverNumber)
+ expect(names[ 2 ]).to.equal('private ' + server.serverNumber)
+ }
+ }
+ }
+ })
+ })
+
+ after(async function () {
+ killallServers(servers)
+
+ // Keep the logs if the test failed
+ if (this['ok']) {
+ await flushTests()
+ }
+ })
+})
import { NSFWQuery } from './nsfw-query.model'
+import { VideoFilter } from '../videos'
export interface VideosSearchQuery {
search?: string
durationMin?: number // seconds
durationMax?: number // seconds
+
+ filter?: VideoFilter
}
REMOVE_ANY_VIDEO_CHANNEL,
REMOVE_ANY_VIDEO_COMMENT,
UPDATE_ANY_VIDEO,
+ SEE_ALL_VIDEOS,
CHANGE_VIDEO_OWNERSHIP
}
UserRight.REMOVE_ANY_VIDEO,
UserRight.REMOVE_ANY_VIDEO_CHANNEL,
UserRight.REMOVE_ANY_VIDEO_COMMENT,
- UserRight.UPDATE_ANY_VIDEO
+ UserRight.UPDATE_ANY_VIDEO,
+ UserRight.SEE_ALL_VIDEOS
],
[UserRole.USER]: []
-export type VideoFilter = 'local'
+export type VideoFilter = 'local' | 'all-local'