aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+page-not-found/page-not-found.component.ts3
-rw-r--r--client/src/app/app-routing.module.ts28
-rw-r--r--client/src/app/shared/form-validators/user-validators.ts4
-rw-r--r--client/src/app/shared/shared-main/account/actor.service.ts37
-rw-r--r--client/src/app/shared/shared-main/account/index.ts1
-rw-r--r--client/src/app/shared/shared-main/index.ts3
-rw-r--r--client/src/app/shared/shared-main/router/actor-redirect-guard.service.ts46
-rw-r--r--client/src/app/shared/shared-main/router/index.ts1
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts10
-rw-r--r--server/controllers/api/actor.ts37
-rw-r--r--server/controllers/api/index.ts2
-rw-r--r--server/helpers/custom-validators/actor.ts10
-rw-r--r--server/helpers/middlewares/video-channels.ts20
-rw-r--r--server/lib/client-html.ts12
-rw-r--r--server/middlewares/validators/actor.ts59
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/tests/api/check-params/actors.ts37
-rw-r--r--server/tests/client.ts270
-rw-r--r--shared/extra-utils/actors/actors.ts18
-rw-r--r--shared/extra-utils/index.ts1
-rw-r--r--shared/extra-utils/requests/requests.ts2
21 files changed, 213 insertions, 389 deletions
diff --git a/client/src/app/+page-not-found/page-not-found.component.ts b/client/src/app/+page-not-found/page-not-found.component.ts
index 94b4c8d27..695568898 100644
--- a/client/src/app/+page-not-found/page-not-found.component.ts
+++ b/client/src/app/+page-not-found/page-not-found.component.ts
@@ -3,6 +3,7 @@ import { Title } from '@angular/platform-browser'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 4import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
5 5
6
6@Component({ 7@Component({
7 selector: 'my-page-not-found', 8 selector: 'my-page-not-found',
8 templateUrl: './page-not-found.component.html', 9 templateUrl: './page-not-found.component.html',
@@ -10,7 +11,7 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
10}) 11})
11export class PageNotFoundComponent implements OnInit { 12export class PageNotFoundComponent implements OnInit {
12 status = HttpStatusCode.NOT_FOUND_404 13 status = HttpStatusCode.NOT_FOUND_404
13 type: string 14 type: 'video' | 'other' = 'other'
14 15
15 public constructor ( 16 public constructor (
16 private titleService: Title, 17 private titleService: Title,
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index 4619c4046..444b6f134 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -5,7 +5,8 @@ import { MenuGuards } from '@app/core/routing/menu-guard.service'
5import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n' 5import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
6import { MetaGuard, PreloadSelectedModulesList } from './core' 6import { MetaGuard, PreloadSelectedModulesList } from './core'
7import { EmptyComponent } from './empty.component' 7import { EmptyComponent } from './empty.component'
8import { RootComponent } from './root.component' 8import { USER_USERNAME_REGEX_CHARACTERS } from './shared/form-validators/user-validators'
9import { ActorRedirectGuard } from './shared/shared-main'
9 10
10const routes: Routes = [ 11const routes: Routes = [
11 { 12 {
@@ -17,7 +18,8 @@ const routes: Routes = [
17 }, 18 },
18 { 19 {
19 path: 'home', 20 path: 'home',
20 loadChildren: () => import('./+home/home.module').then(m => m.HomeModule) 21 loadChildren: () => import('./+home/home.module').then(m => m.HomeModule),
22 canActivateChild: [ MetaGuard ]
21 }, 23 },
22 { 24 {
23 path: 'my-account', 25 path: 'my-account',
@@ -94,18 +96,22 @@ const routes: Routes = [
94 { 96 {
95 matcher: (url): UrlMatchResult => { 97 matcher: (url): UrlMatchResult => {
96 // Matches /@:actorName 98 // Matches /@:actorName
97 if (url.length === 1 && url[0].path.match(/^@[\w]+$/gm)) { 99 const regex = new RegExp(`^@(${USER_USERNAME_REGEX_CHARACTERS}+)$`)
98 return { 100 if (url.length !== 1) return null
99 consumed: url, 101
100 posParams: { 102 const matchResult = url[0].path.match(regex)
101 actorName: new UrlSegment(url[0].path.substr(1), {}) 103 if (!matchResult) return null
102 } 104
105 return {
106 consumed: url,
107 posParams: {
108 actorName: new UrlSegment(matchResult[1], {})
103 } 109 }
104 } 110 }
105
106 return null
107 }, 111 },
108 component: RootComponent 112 pathMatch: 'full',
113 canActivate: [ ActorRedirectGuard ],
114 component: EmptyComponent
109 }, 115 },
110 { 116 {
111 path: '', 117 path: '',
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts
index fee37e95f..976c97b87 100644
--- a/client/src/app/shared/form-validators/user-validators.ts
+++ b/client/src/app/shared/form-validators/user-validators.ts
@@ -1,12 +1,14 @@
1import { Validators } from '@angular/forms' 1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model' 2import { BuildFormValidator } from './form-validator.model'
3 3
4export const USER_USERNAME_REGEX_CHARACTERS = '[a-z0-9][a-z0-9._]'
5
4export const USER_USERNAME_VALIDATOR: BuildFormValidator = { 6export const USER_USERNAME_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [ 7 VALIDATORS: [
6 Validators.required, 8 Validators.required,
7 Validators.minLength(1), 9 Validators.minLength(1),
8 Validators.maxLength(50), 10 Validators.maxLength(50),
9 Validators.pattern(/^[a-z0-9][a-z0-9._]*$/) 11 Validators.pattern(new RegExp(`^${USER_USERNAME_REGEX_CHARACTERS}*$`))
10 ], 12 ],
11 MESSAGES: { 13 MESSAGES: {
12 'required': $localize`Username is required.`, 14 'required': $localize`Username is required.`,
diff --git a/client/src/app/shared/shared-main/account/actor.service.ts b/client/src/app/shared/shared-main/account/actor.service.ts
deleted file mode 100644
index 464ed4519..000000000
--- a/client/src/app/shared/shared-main/account/actor.service.ts
+++ /dev/null
@@ -1,37 +0,0 @@
1import { Observable, ReplaySubject } from 'rxjs'
2import { catchError, map, tap } from 'rxjs/operators'
3import { HttpClient } from '@angular/common/http'
4import { Injectable } from '@angular/core'
5import { RestExtractor } from '@app/core'
6import { Account as ServerAccount, VideoChannel as ServerVideoChannel } from '@shared/models'
7import { environment } from '../../../../environments/environment'
8
9type KeysOfUnion<T> = T extends T ? keyof T: never
10type ServerActor = KeysOfUnion<ServerAccount | ServerVideoChannel>
11
12@Injectable()
13export class ActorService {
14 static BASE_ACTOR_API_URL = environment.apiUrl + '/api/v1/actors/'
15
16 actorLoaded = new ReplaySubject<string>(1)
17
18 constructor (
19 private authHttp: HttpClient,
20 private restExtractor: RestExtractor
21 ) {}
22
23 getActorType (actorName: string): Observable<string> {
24 return this.authHttp.get<ServerActor>(ActorService.BASE_ACTOR_API_URL + actorName)
25 .pipe(
26 map(actorHash => {
27 if (actorHash[ 'userId' ]) {
28 return 'Account'
29 }
30
31 return 'VideoChannel'
32 }),
33 tap(actor => this.actorLoaded.next(actor)),
34 catchError(res => this.restExtractor.handleError(res))
35 )
36 }
37}
diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts
index c6cdcd574..b80ddb9f5 100644
--- a/client/src/app/shared/shared-main/account/index.ts
+++ b/client/src/app/shared/shared-main/account/index.ts
@@ -1,4 +1,3 @@
1export * from './account.model' 1export * from './account.model'
2export * from './account.service' 2export * from './account.service'
3export * from './actor.model' 3export * from './actor.model'
4export * from './actor.service'
diff --git a/client/src/app/shared/shared-main/index.ts b/client/src/app/shared/shared-main/index.ts
index a4d813c06..3a7fd4c34 100644
--- a/client/src/app/shared/shared-main/index.ts
+++ b/client/src/app/shared/shared-main/index.ts
@@ -5,6 +5,9 @@ export * from './date'
5export * from './feeds' 5export * from './feeds'
6export * from './loaders' 6export * from './loaders'
7export * from './misc' 7export * from './misc'
8export * from './peertube-modal'
9export * from './plugins'
10export * from './router'
8export * from './users' 11export * from './users'
9export * from './video' 12export * from './video'
10export * from './video-caption' 13export * from './video-caption'
diff --git a/client/src/app/shared/shared-main/router/actor-redirect-guard.service.ts b/client/src/app/shared/shared-main/router/actor-redirect-guard.service.ts
new file mode 100644
index 000000000..49d61f945
--- /dev/null
+++ b/client/src/app/shared/shared-main/router/actor-redirect-guard.service.ts
@@ -0,0 +1,46 @@
1import { forkJoin, of } from 'rxjs'
2import { catchError, map } from 'rxjs/operators'
3import { Injectable } from '@angular/core'
4import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router'
5import { AccountService } from '../account'
6import { VideoChannelService } from '../video-channel'
7
8@Injectable()
9export class ActorRedirectGuard implements CanActivate {
10
11 constructor (
12 private router: Router,
13 private accountService: AccountService,
14 private channelService: VideoChannelService
15 ) {}
16
17 canActivate (route: ActivatedRouteSnapshot) {
18 const actorName = route.params.actorName
19
20 return forkJoin([
21 this.accountService.getAccount(actorName).pipe(this.orUndefined()),
22 this.channelService.getVideoChannel(actorName).pipe(this.orUndefined())
23 ]).pipe(
24 map(([ account, channel ]) => {
25 if (!account && !channel) {
26 this.router.navigate([ '/404' ])
27 return false
28 }
29
30 if (account) {
31 this.router.navigate([ `/a/${actorName}` ], { skipLocationChange: true })
32 }
33
34 if (channel) {
35 this.router.navigate([ `/c/${actorName}` ], { skipLocationChange: true })
36 }
37
38 return true
39 })
40 )
41 }
42
43 private orUndefined () {
44 return catchError(() => of(undefined))
45 }
46}
diff --git a/client/src/app/shared/shared-main/router/index.ts b/client/src/app/shared/shared-main/router/index.ts
new file mode 100644
index 000000000..f4000b674
--- /dev/null
+++ b/client/src/app/shared/shared-main/router/index.ts
@@ -0,0 +1 @@
export * from './actor-redirect-guard.service'
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index f06f25ca5..c8dd01429 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -4,7 +4,7 @@ import { CommonModule, DatePipe } from '@angular/common'
4import { HttpClientModule } from '@angular/common/http' 4import { HttpClientModule } from '@angular/common/http'
5import { NgModule } from '@angular/core' 5import { NgModule } from '@angular/core'
6import { FormsModule, ReactiveFormsModule } from '@angular/forms' 6import { FormsModule, ReactiveFormsModule } from '@angular/forms'
7import { RouterModule } from '@angular/router' 7import { ActivatedRouteSnapshot, RouterModule } from '@angular/router'
8import { 8import {
9 NgbButtonsModule, 9 NgbButtonsModule,
10 NgbCollapseModule, 10 NgbCollapseModule,
@@ -17,7 +17,7 @@ import {
17import { LoadingBarModule } from '@ngx-loading-bar/core' 17import { LoadingBarModule } from '@ngx-loading-bar/core'
18import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' 18import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
19import { SharedGlobalIconModule } from '../shared-icons' 19import { SharedGlobalIconModule } from '../shared-icons'
20import { AccountService, ActorService } from './account' 20import { AccountService } from './account'
21import { 21import {
22 AutofocusDirective, 22 AutofocusDirective,
23 BytesPipe, 23 BytesPipe,
@@ -39,6 +39,7 @@ import { UserHistoryService, UserNotificationsComponent, UserNotificationService
39import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' 39import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
40import { VideoCaptionService } from './video-caption' 40import { VideoCaptionService } from './video-caption'
41import { VideoChannelService } from './video-channel' 41import { VideoChannelService } from './video-channel'
42import { ActorRedirectGuard } from './router'
42 43
43@NgModule({ 44@NgModule({
44 imports: [ 45 imports: [
@@ -161,7 +162,6 @@ import { VideoChannelService } from './video-channel'
161 AUTH_INTERCEPTOR_PROVIDER, 162 AUTH_INTERCEPTOR_PROVIDER,
162 163
163 AccountService, 164 AccountService,
164 ActorService,
165 165
166 UserHistoryService, 166 UserHistoryService,
167 UserNotificationService, 167 UserNotificationService,
@@ -175,7 +175,9 @@ import { VideoChannelService } from './video-channel'
175 175
176 VideoChannelService, 176 VideoChannelService,
177 177
178 CustomPageService 178 CustomPageService,
179
180 ActorRedirectGuard
179 ] 181 ]
180}) 182})
181export class SharedMainModule { } 183export class SharedMainModule { }
diff --git a/server/controllers/api/actor.ts b/server/controllers/api/actor.ts
deleted file mode 100644
index da7f2eb91..000000000
--- a/server/controllers/api/actor.ts
+++ /dev/null
@@ -1,37 +0,0 @@
1import * as express from 'express'
2import { JobQueue } from '../../lib/job-queue'
3import { asyncMiddleware } from '../../middlewares'
4import { actorNameWithHostGetValidator } from '../../middlewares/validators'
5
6const actorRouter = express.Router()
7
8actorRouter.get('/:actorName',
9 asyncMiddleware(actorNameWithHostGetValidator),
10 getActor
11)
12
13// ---------------------------------------------------------------------------
14
15export {
16 actorRouter
17}
18
19// ---------------------------------------------------------------------------
20
21function getActor (req: express.Request, res: express.Response) {
22 let accountOrVideoChannel
23
24 if (res.locals.account) {
25 accountOrVideoChannel = res.locals.account
26 }
27
28 if (res.locals.videoChannel) {
29 accountOrVideoChannel = res.locals.videoChannel
30 }
31
32 if (accountOrVideoChannel.isOutdated()) {
33 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: accountOrVideoChannel.Actor.url } })
34 }
35
36 return res.json(accountOrVideoChannel.toFormattedJSON())
37}
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 9ffcf1337..28378654a 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -16,7 +16,6 @@ import { pluginRouter } from './plugins'
16import { searchRouter } from './search' 16import { searchRouter } from './search'
17import { serverRouter } from './server' 17import { serverRouter } from './server'
18import { usersRouter } from './users' 18import { usersRouter } from './users'
19import { actorRouter } from './actor'
20import { videoChannelRouter } from './video-channel' 19import { videoChannelRouter } from './video-channel'
21import { videoPlaylistRouter } from './video-playlist' 20import { videoPlaylistRouter } from './video-playlist'
22import { videosRouter } from './videos' 21import { videosRouter } from './videos'
@@ -41,7 +40,6 @@ apiRouter.use('/bulk', bulkRouter)
41apiRouter.use('/oauth-clients', oauthClientsRouter) 40apiRouter.use('/oauth-clients', oauthClientsRouter)
42apiRouter.use('/config', configRouter) 41apiRouter.use('/config', configRouter)
43apiRouter.use('/users', usersRouter) 42apiRouter.use('/users', usersRouter)
44apiRouter.use('/actors', actorRouter)
45apiRouter.use('/accounts', accountsRouter) 43apiRouter.use('/accounts', accountsRouter)
46apiRouter.use('/video-channels', videoChannelRouter) 44apiRouter.use('/video-channels', videoChannelRouter)
47apiRouter.use('/video-playlists', videoPlaylistRouter) 45apiRouter.use('/video-playlists', videoPlaylistRouter)
diff --git a/server/helpers/custom-validators/actor.ts b/server/helpers/custom-validators/actor.ts
deleted file mode 100644
index ad129e080..000000000
--- a/server/helpers/custom-validators/actor.ts
+++ /dev/null
@@ -1,10 +0,0 @@
1import { isAccountNameValid } from './accounts'
2import { isVideoChannelNameValid } from './video-channels'
3
4function isActorNameValid (value: string) {
5 return isAccountNameValid(value) || isVideoChannelNameValid(value)
6}
7
8export {
9 isActorNameValid
10}
diff --git a/server/helpers/middlewares/video-channels.ts b/server/helpers/middlewares/video-channels.ts
index e30ea90b3..602555921 100644
--- a/server/helpers/middlewares/video-channels.ts
+++ b/server/helpers/middlewares/video-channels.ts
@@ -3,22 +3,22 @@ import { MChannelBannerAccountDefault } from '@server/types/models'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { VideoChannelModel } from '../../models/video/video-channel' 4import { VideoChannelModel } from '../../models/video/video-channel'
5 5
6async function doesLocalVideoChannelNameExist (name: string, res: express.Response, sendNotFound = true) { 6async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
7 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) 7 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
8 8
9 return processVideoChannelExist(videoChannel, res, sendNotFound) 9 return processVideoChannelExist(videoChannel, res)
10} 10}
11 11
12async function doesVideoChannelIdExist (id: number, res: express.Response, sendNotFound = true) { 12async function doesVideoChannelIdExist (id: number, res: express.Response) {
13 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id) 13 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
14 14
15 return processVideoChannelExist(videoChannel, res, sendNotFound) 15 return processVideoChannelExist(videoChannel, res)
16} 16}
17 17
18async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response, sendNotFound = true) { 18async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
19 const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain) 19 const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
20 20
21 return processVideoChannelExist(videoChannel, res, sendNotFound) 21 return processVideoChannelExist(videoChannel, res)
22} 22}
23 23
24// --------------------------------------------------------------------------- 24// ---------------------------------------------------------------------------
@@ -29,12 +29,10 @@ export {
29 doesVideoChannelNameWithHostExist 29 doesVideoChannelNameWithHostExist
30} 30}
31 31
32function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response, sendNotFound = true) { 32function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) {
33 if (!videoChannel) { 33 if (!videoChannel) {
34 if (sendNotFound) { 34 res.status(HttpStatusCode.NOT_FOUND_404)
35 res.status(HttpStatusCode.NOT_FOUND_404) 35 .json({ error: 'Video channel not found' })
36 .json({ error: 'Video channel not found' })
37 }
38 36
39 return false 37 return false
40 } 38 }
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 2f6bce1c7..3c09332b5 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -208,14 +208,12 @@ class ClientHtml {
208 } 208 }
209 209
210 static async getActorHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) { 210 static async getActorHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
211 const accountModel = await AccountModel.loadByNameWithHost(nameWithHost) 211 const [ account, channel ] = await Promise.all([
212 AccountModel.loadByNameWithHost(nameWithHost),
213 VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost)
214 ])
212 215
213 if (accountModel) { 216 return this.getAccountOrChannelHTMLPage(() => Promise.resolve(account || channel), req, res)
214 return this.getAccountOrChannelHTMLPage(() => new Promise(resolve => resolve(accountModel)), req, res)
215 } else {
216 const videoChannelModelPromise = VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost)
217 return this.getAccountOrChannelHTMLPage(() => videoChannelModelPromise, req, res)
218 }
219 } 217 }
220 218
221 static async getEmbedHTML () { 219 static async getEmbedHTML () {
diff --git a/server/middlewares/validators/actor.ts b/server/middlewares/validators/actor.ts
deleted file mode 100644
index 99b529dd6..000000000
--- a/server/middlewares/validators/actor.ts
+++ /dev/null
@@ -1,59 +0,0 @@
1import * as express from 'express'
2import { param } from 'express-validator'
3import { isActorNameValid } from '../../helpers/custom-validators/actor'
4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils'
6import {
7 doesAccountNameWithHostExist,
8 doesLocalAccountNameExist,
9 doesVideoChannelNameWithHostExist,
10 doesLocalVideoChannelNameExist
11} from '../../helpers/middlewares'
12import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
13
14const localActorValidator = [
15 param('actorName').custom(isActorNameValid).withMessage('Should have a valid actor name'),
16
17 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
18 logger.debug('Checking localActorValidator parameters', { parameters: req.params })
19
20 if (areValidationErrors(req, res)) return
21
22 const isAccount = await doesLocalAccountNameExist(req.params.actorName, res, false)
23 const isVideoChannel = await doesLocalVideoChannelNameExist(req.params.actorName, res, false)
24
25 if (!isAccount || !isVideoChannel) {
26 res.status(HttpStatusCode.NOT_FOUND_404)
27 .json({ error: 'Actor not found' })
28 }
29
30 return next()
31 }
32]
33
34const actorNameWithHostGetValidator = [
35 param('actorName').exists().withMessage('Should have an actor name with host'),
36
37 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
38 logger.debug('Checking actorNameWithHostGetValidator parameters', { parameters: req.params })
39
40 if (areValidationErrors(req, res)) return
41
42 const isAccount = await doesAccountNameWithHostExist(req.params.actorName, res, false)
43 const isVideoChannel = await doesVideoChannelNameWithHostExist(req.params.actorName, res, false)
44
45 if (!isAccount && !isVideoChannel) {
46 res.status(HttpStatusCode.NOT_FOUND_404)
47 .json({ error: 'Actor not found' })
48 }
49
50 return next()
51 }
52]
53
54// ---------------------------------------------------------------------------
55
56export {
57 localActorValidator,
58 actorNameWithHostGetValidator
59}
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 3e1a1e5ce..24faeea3e 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -1,6 +1,5 @@
1export * from './abuse' 1export * from './abuse'
2export * from './account' 2export * from './account'
3export * from './actor'
4export * from './actor-image' 3export * from './actor-image'
5export * from './blocklist' 4export * from './blocklist'
6export * from './oembed' 5export * from './oembed'
diff --git a/server/tests/api/check-params/actors.ts b/server/tests/api/check-params/actors.ts
deleted file mode 100644
index 3a03edc39..000000000
--- a/server/tests/api/check-params/actors.ts
+++ /dev/null
@@ -1,37 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4
5import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../../shared/extra-utils'
6import { getActor } from '../../../../shared/extra-utils/actors/actors'
7import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
8
9describe('Test actors API validators', function () {
10 let server: ServerInfo
11
12 // ---------------------------------------------------------------
13
14 before(async function () {
15 this.timeout(30000)
16
17 server = await flushAndRunServer(1)
18 })
19
20 describe('When getting an actor', function () {
21 it('Should return 404 with a non existing actorName', async function () {
22 await getActor(server.url, 'arfaze', HttpStatusCode.NOT_FOUND_404)
23 })
24
25 it('Should return 200 with an existing accountName', async function () {
26 await getActor(server.url, 'root', HttpStatusCode.OK_200)
27 })
28
29 it('Should return 200 with an existing channelName', async function () {
30 await getActor(server.url, 'root_channel', HttpStatusCode.OK_200)
31 })
32 })
33
34 after(async function () {
35 await cleanupTests([ server ])
36 })
37})
diff --git a/server/tests/client.ts b/server/tests/client.ts
index d9a472fdd..f33e5c1da 100644
--- a/server/tests/client.ts
+++ b/server/tests/client.ts
@@ -2,8 +2,10 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { omit } from 'lodash'
5import * as request from 'supertest' 6import * as request from 'supertest'
6import { Account, HTMLServerConfig, ServerConfig, VideoPlaylistPrivacy } from '@shared/models' 7import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
8import { Account, CustomConfig, HTMLServerConfig, ServerConfig, VideoPlaylistPrivacy } from '@shared/models'
7import { 9import {
8 addVideoInPlaylist, 10 addVideoInPlaylist,
9 cleanupTests, 11 cleanupTests,
@@ -14,6 +16,7 @@ import {
14 getConfig, 16 getConfig,
15 getCustomConfig, 17 getCustomConfig,
16 getVideosList, 18 getVideosList,
19 makeGetRequest,
17 makeHTMLRequest, 20 makeHTMLRequest,
18 ServerInfo, 21 ServerInfo,
19 setAccessTokensToServers, 22 setAccessTokensToServers,
@@ -25,8 +28,6 @@ import {
25 uploadVideo, 28 uploadVideo,
26 waitJobs 29 waitJobs
27} from '../../shared/extra-utils' 30} from '../../shared/extra-utils'
28import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
29import { omit } from 'lodash'
30 31
31const expect = chai.expect 32const expect = chai.expect
32 33
@@ -144,52 +145,36 @@ describe('Test a client controllers', function () {
144 145
145 describe('Open Graph', function () { 146 describe('Open Graph', function () {
146 147
147 it('Should have valid Open Graph tags on the account page', async function () { 148 async function accountPageTest (path: string) {
148 const accountPageTests = (res) => { 149 const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
149 expect(res.text).to.contain(`<meta property="og:title" content="${account.displayName}" />`) 150 const text = res.text
150 expect(res.text).to.contain(`<meta property="og:description" content="${account.description}" />`)
151 expect(res.text).to.contain('<meta property="og:type" content="website" />')
152 expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/accounts/${servers[0].user.username}" />`)
153 }
154 151
155 accountPageTests(await request(servers[0].url) 152 expect(text).to.contain(`<meta property="og:title" content="${account.displayName}" />`)
156 .get('/accounts/' + servers[0].user.username) 153 expect(text).to.contain(`<meta property="og:description" content="${account.description}" />`)
157 .set('Accept', 'text/html') 154 expect(text).to.contain('<meta property="og:type" content="website" />')
158 .expect(HttpStatusCode.OK_200)) 155 expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/accounts/${servers[0].user.username}" />`)
156 }
159 157
160 accountPageTests(await request(servers[0].url) 158 async function channelPageTest (path: string) {
161 .get('/a/' + servers[0].user.username) 159 const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
162 .set('Accept', 'text/html') 160 const text = res.text
163 .expect(HttpStatusCode.OK_200))
164 161
165 accountPageTests(await request(servers[0].url) 162 expect(text).to.contain(`<meta property="og:title" content="${servers[0].videoChannel.displayName}" />`)
166 .get('/@' + servers[0].user.username) 163 expect(text).to.contain(`<meta property="og:description" content="${channelDescription}" />`)
167 .set('Accept', 'text/html') 164 expect(text).to.contain('<meta property="og:type" content="website" />')
168 .expect(HttpStatusCode.OK_200)) 165 expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/video-channels/${servers[0].videoChannel.name}" />`)
166 }
167
168 it('Should have valid Open Graph tags on the account page', async function () {
169 await accountPageTest('/accounts/' + servers[0].user.username)
170 await accountPageTest('/a/' + servers[0].user.username)
171 await accountPageTest('/@' + servers[0].user.username)
169 }) 172 })
170 173
171 it('Should have valid Open Graph tags on the channel page', async function () { 174 it('Should have valid Open Graph tags on the channel page', async function () {
172 const channelPageOGtests = (res) => { 175 await channelPageTest('/video-channels/' + servers[0].videoChannel.name)
173 expect(res.text).to.contain(`<meta property="og:title" content="${servers[0].videoChannel.displayName}" />`) 176 await channelPageTest('/c/' + servers[0].videoChannel.name)
174 expect(res.text).to.contain(`<meta property="og:description" content="${channelDescription}" />`) 177 await channelPageTest('/@' + servers[0].videoChannel.name)
175 expect(res.text).to.contain('<meta property="og:type" content="website" />')
176 expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/video-channels/${servers[0].videoChannel.name}" />`)
177 }
178
179 channelPageOGtests(await request(servers[0].url)
180 .get('/video-channels/' + servers[0].videoChannel.name)
181 .set('Accept', 'text/html')
182 .expect(HttpStatusCode.OK_200))
183
184 channelPageOGtests(await request(servers[0].url)
185 .get('/c/' + servers[0].videoChannel.name)
186 .set('Accept', 'text/html')
187 .expect(HttpStatusCode.OK_200))
188
189 channelPageOGtests(await request(servers[0].url)
190 .get('/@' + servers[0].videoChannel.name)
191 .set('Accept', 'text/html')
192 .expect(HttpStatusCode.OK_200))
193 }) 178 })
194 179
195 it('Should have valid Open Graph tags on the watch page with video id', async function () { 180 it('Should have valid Open Graph tags on the watch page with video id', async function () {
@@ -231,142 +216,125 @@ describe('Test a client controllers', function () {
231 216
232 describe('Twitter card', async function () { 217 describe('Twitter card', async function () {
233 218
234 it('Should have valid twitter card on the watch video page', async function () { 219 describe('Not whitelisted', function () {
235 const res = await request(servers[0].url)
236 .get('/videos/watch/' + servers[0].video.uuid)
237 .set('Accept', 'text/html')
238 .expect(HttpStatusCode.OK_200)
239 220
240 expect(res.text).to.contain('<meta property="twitter:card" content="summary_large_image" />') 221 async function accountPageTest (path: string) {
241 expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') 222 const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
242 expect(res.text).to.contain(`<meta property="twitter:title" content="${videoName}" />`) 223 const text = res.text
243 expect(res.text).to.contain(`<meta property="twitter:description" content="${videoDescriptionPlainText}" />`)
244 })
245 224
246 it('Should have valid twitter card on the watch playlist page', async function () { 225 expect(text).to.contain('<meta property="twitter:card" content="summary" />')
247 const res = await request(servers[0].url) 226 expect(text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
248 .get('/videos/watch/playlist/' + playlistUUID) 227 expect(text).to.contain(`<meta property="twitter:title" content="${account.name}" />`)
249 .set('Accept', 'text/html') 228 expect(text).to.contain(`<meta property="twitter:description" content="${account.description}" />`)
250 .expect(HttpStatusCode.OK_200) 229 }
251 230
252 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />') 231 async function channelPageTest (path: string) {
253 expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') 232 const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
254 expect(res.text).to.contain(`<meta property="twitter:title" content="${playlistName}" />`) 233 const text = res.text
255 expect(res.text).to.contain(`<meta property="twitter:description" content="${playlistDescription}" />`)
256 })
257 234
258 it('Should have valid twitter card on the account page', async function () { 235 expect(text).to.contain('<meta property="twitter:card" content="summary" />')
259 const accountPageTests = (res) => { 236 expect(text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
260 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />') 237 expect(text).to.contain(`<meta property="twitter:title" content="${servers[0].videoChannel.displayName}" />`)
261 expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') 238 expect(text).to.contain(`<meta property="twitter:description" content="${channelDescription}" />`)
262 expect(res.text).to.contain(`<meta property="twitter:title" content="${account.name}" />`)
263 expect(res.text).to.contain(`<meta property="twitter:description" content="${account.description}" />`)
264 } 239 }
265 240
266 accountPageTests(await request(servers[0].url) 241 it('Should have valid twitter card on the watch video page', async function () {
267 .get('/accounts/' + account.name) 242 const res = await request(servers[0].url)
268 .set('Accept', 'text/html') 243 .get('/videos/watch/' + servers[0].video.uuid)
269 .expect(HttpStatusCode.OK_200)) 244 .set('Accept', 'text/html')
245 .expect(HttpStatusCode.OK_200)
270 246
271 accountPageTests(await request(servers[0].url) 247 expect(res.text).to.contain('<meta property="twitter:card" content="summary_large_image" />')
272 .get('/a/' + account.name) 248 expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
273 .set('Accept', 'text/html') 249 expect(res.text).to.contain(`<meta property="twitter:title" content="${videoName}" />`)
274 .expect(HttpStatusCode.OK_200)) 250 expect(res.text).to.contain(`<meta property="twitter:description" content="${videoDescriptionPlainText}" />`)
251 })
275 252
276 accountPageTests(await request(servers[0].url) 253 it('Should have valid twitter card on the watch playlist page', async function () {
277 .get('/@' + account.name) 254 const res = await request(servers[0].url)
278 .set('Accept', 'text/html') 255 .get('/videos/watch/playlist/' + playlistUUID)
279 .expect(HttpStatusCode.OK_200)) 256 .set('Accept', 'text/html')
280 }) 257 .expect(HttpStatusCode.OK_200)
281 258
282 it('Should have valid twitter card on the channel page', async function () {
283 const channelPageTests = (res) => {
284 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />') 259 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
285 expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') 260 expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
286 expect(res.text).to.contain(`<meta property="twitter:title" content="${servers[0].videoChannel.displayName}" />`) 261 expect(res.text).to.contain(`<meta property="twitter:title" content="${playlistName}" />`)
287 expect(res.text).to.contain(`<meta property="twitter:description" content="${channelDescription}" />`) 262 expect(res.text).to.contain(`<meta property="twitter:description" content="${playlistDescription}" />`)
288 } 263 })
289
290 channelPageTests(await request(servers[0].url)
291 .get('/video-channels/' + servers[0].videoChannel.name)
292 .set('Accept', 'text/html')
293 .expect(HttpStatusCode.OK_200))
294 264
295 channelPageTests(await request(servers[0].url) 265 it('Should have valid twitter card on the account page', async function () {
296 .get('/c/' + servers[0].videoChannel.name) 266 await accountPageTest('/accounts/' + account.name)
297 .set('Accept', 'text/html') 267 await accountPageTest('/a/' + account.name)
298 .expect(HttpStatusCode.OK_200)) 268 await accountPageTest('/@' + account.name)
269 })
299 270
300 channelPageTests(await request(servers[0].url) 271 it('Should have valid twitter card on the channel page', async function () {
301 .get('/@' + servers[0].videoChannel.name) 272 await channelPageTest('/video-channels/' + servers[0].videoChannel.name)
302 .set('Accept', 'text/html') 273 await channelPageTest('/c/' + servers[0].videoChannel.name)
303 .expect(HttpStatusCode.OK_200)) 274 await channelPageTest('/@' + servers[0].videoChannel.name)
275 })
304 }) 276 })
305 277
306 it('Should have valid twitter card if Twitter is whitelisted', async function () { 278 describe('Whitelisted', function () {
307 const res1 = await getCustomConfig(servers[0].url, servers[0].accessToken)
308 const config = res1.body
309 config.services.twitter = {
310 username: '@Kuja',
311 whitelisted: true
312 }
313 await updateCustomConfig(servers[0].url, servers[0].accessToken, config)
314
315 const resVideoRequest = await request(servers[0].url)
316 .get('/videos/watch/' + servers[0].video.uuid)
317 .set('Accept', 'text/html')
318 .expect(HttpStatusCode.OK_200)
319 279
320 expect(resVideoRequest.text).to.contain('<meta property="twitter:card" content="player" />') 280 before(async function () {
321 expect(resVideoRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />') 281 const res = await getCustomConfig(servers[0].url, servers[0].accessToken)
282 const config = res.body as CustomConfig
283 config.services.twitter = {
284 username: '@Kuja',
285 whitelisted: true
286 }
322 287
323 const resVideoPlaylistRequest = await request(servers[0].url) 288 await updateCustomConfig(servers[0].url, servers[0].accessToken, config)
324 .get('/videos/watch/playlist/' + playlistUUID) 289 })
325 .set('Accept', 'text/html')
326 .expect(HttpStatusCode.OK_200)
327 290
328 expect(resVideoPlaylistRequest.text).to.contain('<meta property="twitter:card" content="player" />') 291 async function accountPageTest (path: string) {
329 expect(resVideoPlaylistRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />') 292 const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
293 const text = res.text
330 294
331 const accountTests = (res) => { 295 expect(text).to.contain('<meta property="twitter:card" content="summary" />')
332 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />') 296 expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />')
333 expect(res.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
334 } 297 }
335 298
336 accountTests(await request(servers[0].url) 299 async function channelPageTest (path: string) {
337 .get('/accounts/' + account.name) 300 const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 })
338 .set('Accept', 'text/html') 301 const text = res.text
339 .expect(HttpStatusCode.OK_200))
340 302
341 accountTests(await request(servers[0].url) 303 expect(text).to.contain('<meta property="twitter:card" content="summary" />')
342 .get('/a/' + account.name) 304 expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />')
343 .set('Accept', 'text/html') 305 }
344 .expect(HttpStatusCode.OK_200))
345 306
346 accountTests(await request(servers[0].url) 307 it('Should have valid twitter card on the watch video page', async function () {
347 .get('/@' + account.name) 308 const res = await request(servers[0].url)
348 .set('Accept', 'text/html') 309 .get('/videos/watch/' + servers[0].video.uuid)
349 .expect(HttpStatusCode.OK_200)) 310 .set('Accept', 'text/html')
311 .expect(HttpStatusCode.OK_200)
350 312
351 const channelTests = (res) => { 313 expect(res.text).to.contain('<meta property="twitter:card" content="player" />')
352 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
353 expect(res.text).to.contain('<meta property="twitter:site" content="@Kuja" />') 314 expect(res.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
354 } 315 })
355 316
356 channelTests(await request(servers[0].url) 317 it('Should have valid twitter card on the watch playlist page', async function () {
357 .get('/video-channels/' + servers[0].videoChannel.name) 318 const res = await request(servers[0].url)
358 .set('Accept', 'text/html') 319 .get('/videos/watch/playlist/' + playlistUUID)
359 .expect(HttpStatusCode.OK_200)) 320 .set('Accept', 'text/html')
321 .expect(HttpStatusCode.OK_200)
360 322
361 channelTests(await request(servers[0].url) 323 expect(res.text).to.contain('<meta property="twitter:card" content="player" />')
362 .get('/c/' + servers[0].videoChannel.name) 324 expect(res.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
363 .set('Accept', 'text/html') 325 })
364 .expect(HttpStatusCode.OK_200))
365 326
366 channelTests(await request(servers[0].url) 327 it('Should have valid twitter card on the account page', async function () {
367 .get('/@' + servers[0].videoChannel.name) 328 await accountPageTest('/accounts/' + account.name)
368 .set('Accept', 'text/html') 329 await accountPageTest('/a/' + account.name)
369 .expect(HttpStatusCode.OK_200)) 330 await accountPageTest('/@' + account.name)
331 })
332
333 it('Should have valid twitter card on the channel page', async function () {
334 await channelPageTest('/video-channels/' + servers[0].videoChannel.name)
335 await channelPageTest('/c/' + servers[0].videoChannel.name)
336 await channelPageTest('/@' + servers[0].videoChannel.name)
337 })
370 }) 338 })
371 }) 339 })
372 340
diff --git a/shared/extra-utils/actors/actors.ts b/shared/extra-utils/actors/actors.ts
deleted file mode 100644
index 4a4aba775..000000000
--- a/shared/extra-utils/actors/actors.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { makeGetRequest } from '../requests/requests'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5
6function getActor (url: string, actorName: string, statusCodeExpected = HttpStatusCode.OK_200) {
7 const path = '/api/v1/actors/' + actorName
8
9 return makeGetRequest({
10 url,
11 path,
12 statusCodeExpected
13 })
14}
15
16export {
17 getActor
18}
diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts
index 9f5b5bb28..3bc09ead5 100644
--- a/shared/extra-utils/index.ts
+++ b/shared/extra-utils/index.ts
@@ -1,4 +1,3 @@
1export * from './actors/actors'
2export * from './bulk/bulk' 1export * from './bulk/bulk'
3 2
4export * from './cli/cli' 3export * from './cli/cli'
diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts
index 8b5cddf4a..38e24d897 100644
--- a/shared/extra-utils/requests/requests.ts
+++ b/shared/extra-utils/requests/requests.ts
@@ -26,6 +26,7 @@ function makeGetRequest (options: {
26 contentType?: string 26 contentType?: string
27 range?: string 27 range?: string
28 redirects?: number 28 redirects?: number
29 accept?: string
29}) { 30}) {
30 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400 31 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400
31 if (options.contentType === undefined) options.contentType = 'application/json' 32 if (options.contentType === undefined) options.contentType = 'application/json'
@@ -36,6 +37,7 @@ function makeGetRequest (options: {
36 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 37 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
37 if (options.query) req.query(options.query) 38 if (options.query) req.query(options.query)
38 if (options.range) req.set('Range', options.range) 39 if (options.range) req.set('Range', options.range)
40 if (options.accept) req.set('Accept', options.accept)
39 if (options.redirects) req.redirects(options.redirects) 41 if (options.redirects) req.redirects(options.redirects)
40 42
41 return req.expect(options.statusCodeExpected) 43 return req.expect(options.statusCodeExpected)