aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/proxy.config.json4
-rw-r--r--client/src/app/app.component.ts12
-rw-r--r--client/src/app/core/core.module.ts3
-rw-r--r--client/src/app/core/plugins/plugin.service.ts137
-rw-r--r--client/src/app/core/server/server.service.ts1
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts8
-rw-r--r--server/controllers/api/config.ts19
-rw-r--r--server/lib/plugins/plugin-manager.ts21
-rw-r--r--shared/models/plugins/plugin-scope.type.ts1
-rw-r--r--shared/models/plugins/register.model.ts2
-rw-r--r--shared/models/server/server-config.model.ts10
11 files changed, 215 insertions, 3 deletions
diff --git a/client/proxy.config.json b/client/proxy.config.json
index 4a72f1826..c6300a412 100644
--- a/client/proxy.config.json
+++ b/client/proxy.config.json
@@ -3,6 +3,10 @@
3 "target": "http://localhost:9000", 3 "target": "http://localhost:9000",
4 "secure": false 4 "secure": false
5 }, 5 },
6 "/plugins": {
7 "target": "http://localhost:9000",
8 "secure": false
9 },
6 "/static": { 10 "/static": {
7 "target": "http://localhost:9000", 11 "target": "http://localhost:9000",
8 "secure": false 12 "secure": false
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 915466af7..548173f61 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -9,6 +9,7 @@ import { Hotkey, HotkeysService } from 'angular2-hotkeys'
9import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { fromEvent } from 'rxjs' 10import { fromEvent } from 'rxjs'
11import { ViewportScroller } from '@angular/common' 11import { ViewportScroller } from '@angular/common'
12import { PluginService } from '@app/core/plugins/plugin.service'
12 13
13@Component({ 14@Component({
14 selector: 'my-app', 15 selector: 'my-app',
@@ -27,6 +28,7 @@ export class AppComponent implements OnInit {
27 private router: Router, 28 private router: Router,
28 private authService: AuthService, 29 private authService: AuthService,
29 private serverService: ServerService, 30 private serverService: ServerService,
31 private pluginService: PluginService,
30 private domSanitizer: DomSanitizer, 32 private domSanitizer: DomSanitizer,
31 private redirectService: RedirectService, 33 private redirectService: RedirectService,
32 private screenService: ScreenService, 34 private screenService: ScreenService,
@@ -69,6 +71,8 @@ export class AppComponent implements OnInit {
69 this.serverService.loadVideoPrivacies() 71 this.serverService.loadVideoPrivacies()
70 this.serverService.loadVideoPlaylistPrivacies() 72 this.serverService.loadVideoPlaylistPrivacies()
71 73
74 this.loadPlugins()
75
72 // Do not display menu on small screens 76 // Do not display menu on small screens
73 if (this.screenService.isInSmallView()) { 77 if (this.screenService.isInSmallView()) {
74 this.isMenuDisplayed = false 78 this.isMenuDisplayed = false
@@ -196,6 +200,14 @@ export class AppComponent implements OnInit {
196 }) 200 })
197 } 201 }
198 202
203 private async loadPlugins () {
204 this.pluginService.initializePlugins()
205
206 await this.pluginService.loadPluginsByScope('common')
207
208 this.pluginService.runHook('action:application.loaded')
209 }
210
199 private initHotkeys () { 211 private initHotkeys () {
200 this.hotkeysService.add([ 212 this.hotkeysService.add([
201 new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => { 213 new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index 06fa8fcf1..436c0dfb8 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -21,6 +21,7 @@ import { MessageService } from 'primeng/api'
21import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' 21import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
22import { ServerConfigResolver } from './routing/server-config-resolver.service' 22import { ServerConfigResolver } from './routing/server-config-resolver.service'
23import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' 23import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
24import { PluginService } from '@app/core/plugins/plugin.service'
24 25
25@NgModule({ 26@NgModule({
26 imports: [ 27 imports: [
@@ -61,6 +62,8 @@ import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
61 UserRightGuard, 62 UserRightGuard,
62 UnloggedGuard, 63 UnloggedGuard,
63 64
65 PluginService,
66
64 RedirectService, 67 RedirectService,
65 Notifier, 68 Notifier,
66 MessageService, 69 MessageService,
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
new file mode 100644
index 000000000..6c567d3ca
--- /dev/null
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -0,0 +1,137 @@
1import { Injectable } from '@angular/core'
2import { Router } from '@angular/router'
3import { ServerConfigPlugin } from '@shared/models'
4import { ServerService } from '@app/core/server/server.service'
5import { ClientScript } from '@shared/models/plugins/plugin-package-json.model'
6import { PluginScope } from '@shared/models/plugins/plugin-scope.type'
7import { environment } from '../../../environments/environment'
8import { RegisterHookOptions } from '@shared/models/plugins/register.model'
9import { ReplaySubject } from 'rxjs'
10import { first } from 'rxjs/operators'
11
12interface HookStructValue extends RegisterHookOptions {
13 plugin: ServerConfigPlugin
14 clientScript: ClientScript
15}
16
17@Injectable()
18export class PluginService {
19 pluginsLoaded = new ReplaySubject<boolean>(1)
20
21 private plugins: ServerConfigPlugin[] = []
22 private scopes: { [ scopeName: string ]: { plugin: ServerConfigPlugin, clientScript: ClientScript }[] } = {}
23 private loadedScripts: { [ script: string ]: boolean } = {}
24
25 private hooks: { [ name: string ]: HookStructValue[] } = {}
26
27 constructor (
28 private router: Router,
29 private server: ServerService
30 ) {
31 }
32
33 initializePlugins () {
34 this.server.configLoaded
35 .subscribe(() => {
36 this.plugins = this.server.getConfig().plugins
37
38 this.buildScopeStruct()
39
40 this.pluginsLoaded.next(true)
41 })
42 }
43
44 ensurePluginsAreLoaded () {
45 return this.pluginsLoaded.asObservable()
46 .pipe(first())
47 .toPromise()
48 }
49
50 async loadPluginsByScope (scope: PluginScope) {
51 try {
52 await this.ensurePluginsAreLoaded()
53
54 const toLoad = this.scopes[ scope ]
55 if (!Array.isArray(toLoad)) return
56
57 const promises: Promise<any>[] = []
58 for (const { plugin, clientScript } of toLoad) {
59 if (this.loadedScripts[ clientScript.script ]) continue
60
61 promises.push(this.loadPlugin(plugin, clientScript))
62
63 this.loadedScripts[ clientScript.script ] = true
64 }
65
66 return Promise.all(promises)
67 } catch (err) {
68 console.error('Cannot load plugins by scope %s.', scope, err)
69 }
70 }
71
72 async runHook (hookName: string, param?: any) {
73 let result = param
74
75 const wait = hookName.startsWith('static:')
76
77 for (const hook of this.hooks[hookName]) {
78 try {
79 if (wait) result = await hook.handler(param)
80 else result = hook.handler()
81 } catch (err) {
82 console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.plugin, hook.clientScript, err)
83 }
84 }
85
86 return result
87 }
88
89 private loadPlugin (plugin: ServerConfigPlugin, clientScript: ClientScript) {
90 const registerHook = (options: RegisterHookOptions) => {
91 if (!this.hooks[options.target]) this.hooks[options.target] = []
92
93 this.hooks[options.target].push({
94 plugin,
95 clientScript,
96 target: options.target,
97 handler: options.handler,
98 priority: options.priority || 0
99 })
100 }
101
102 console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
103
104 const url = environment.apiUrl + `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`
105
106 return import(/* webpackIgnore: true */ url)
107 .then(script => script.register({ registerHook }))
108 .then(() => this.sortHooksByPriority())
109 }
110
111 private buildScopeStruct () {
112 for (const plugin of this.plugins) {
113 for (const key of Object.keys(plugin.clientScripts)) {
114 const clientScript = plugin.clientScripts[key]
115
116 for (const scope of clientScript.scopes) {
117 if (!this.scopes[scope]) this.scopes[scope] = []
118
119 this.scopes[scope].push({
120 plugin,
121 clientScript
122 })
123
124 this.loadedScripts[clientScript.script] = false
125 }
126 }
127 }
128 }
129
130 private sortHooksByPriority () {
131 for (const hookName of Object.keys(this.hooks)) {
132 this.hooks[hookName].sort((a, b) => {
133 return b.priority - a.priority
134 })
135 }
136 }
137}
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 689f25a40..80c52164d 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -42,6 +42,7 @@ export class ServerService {
42 css: '' 42 css: ''
43 } 43 }
44 }, 44 },
45 plugins: [],
45 email: { 46 email: {
46 enabled: false 47 enabled: false
47 }, 48 },
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index 3f1a98f89..6d8bb4b3f 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -32,6 +32,7 @@ import { Video } from '@app/shared/video/video.model'
32import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' 32import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils'
33import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component' 33import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
34import { getStoredTheater } from '../../../assets/player/peertube-player-local-storage' 34import { getStoredTheater } from '../../../assets/player/peertube-player-local-storage'
35import { PluginService } from '@app/core/plugins/plugin.service'
35 36
36@Component({ 37@Component({
37 selector: 'my-video-watch', 38 selector: 'my-video-watch',
@@ -85,6 +86,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
85 private serverService: ServerService, 86 private serverService: ServerService,
86 private restExtractor: RestExtractor, 87 private restExtractor: RestExtractor,
87 private notifier: Notifier, 88 private notifier: Notifier,
89 private pluginService: PluginService,
88 private markdownService: MarkdownService, 90 private markdownService: MarkdownService,
89 private zone: NgZone, 91 private zone: NgZone,
90 private redirectService: RedirectService, 92 private redirectService: RedirectService,
@@ -98,7 +100,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
98 return this.authService.getUser() 100 return this.authService.getUser()
99 } 101 }
100 102
101 ngOnInit () { 103 async ngOnInit () {
104 await this.pluginService.loadPluginsByScope('video-watch')
105
102 this.configSub = this.serverService.configLoaded 106 this.configSub = this.serverService.configLoaded
103 .subscribe(() => { 107 .subscribe(() => {
104 if ( 108 if (
@@ -126,6 +130,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
126 this.initHotkeys() 130 this.initHotkeys()
127 131
128 this.theaterEnabled = getStoredTheater() 132 this.theaterEnabled = getStoredTheater()
133
134 this.pluginService.runHook('action:video-watch.loaded')
129 } 135 }
130 136
131 ngOnDestroy () { 137 ngOnDestroy () {
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 1d12f701b..8563b7437 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { snakeCase } from 'lodash' 2import { snakeCase } from 'lodash'
3import { ServerConfig, UserRight } from '../../../shared' 3import { ServerConfig, ServerConfigPlugin, UserRight } from '../../../shared'
4import { About } from '../../../shared/models/server/about.model' 4import { About } from '../../../shared/models/server/about.model'
5import { CustomConfig } from '../../../shared/models/server/custom-config.model' 5import { CustomConfig } from '../../../shared/models/server/custom-config.model'
6import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' 6import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
@@ -15,6 +15,8 @@ import { Emailer } from '../../lib/emailer'
15import { isNumeric } from 'validator' 15import { isNumeric } from 'validator'
16import { objectConverter } from '../../helpers/core-utils' 16import { objectConverter } from '../../helpers/core-utils'
17import { CONFIG, reloadConfig } from '../../initializers/config' 17import { CONFIG, reloadConfig } from '../../initializers/config'
18import { PluginManager } from '../../lib/plugins/plugin-manager'
19import { PluginType } from '../../../shared/models/plugins/plugin.type'
18 20
19const packageJSON = require('../../../../package.json') 21const packageJSON = require('../../../../package.json')
20const configRouter = express.Router() 22const configRouter = express.Router()
@@ -54,6 +56,20 @@ async function getConfig (req: express.Request, res: express.Response) {
54 .filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true) 56 .filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true)
55 .map(r => parseInt(r, 10)) 57 .map(r => parseInt(r, 10))
56 58
59 const plugins: ServerConfigPlugin[] = []
60 const registeredPlugins = PluginManager.Instance.getRegisteredPlugins()
61 for (const pluginName of Object.keys(registeredPlugins)) {
62 const plugin = registeredPlugins[ pluginName ]
63 if (plugin.type !== PluginType.PLUGIN) continue
64
65 plugins.push({
66 name: plugin.name,
67 version: plugin.version,
68 description: plugin.description,
69 clientScripts: plugin.clientScripts
70 })
71 }
72
57 const json: ServerConfig = { 73 const json: ServerConfig = {
58 instance: { 74 instance: {
59 name: CONFIG.INSTANCE.NAME, 75 name: CONFIG.INSTANCE.NAME,
@@ -66,6 +82,7 @@ async function getConfig (req: express.Request, res: express.Response) {
66 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS 82 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
67 } 83 }
68 }, 84 },
85 plugins,
69 email: { 86 email: {
70 enabled: Emailer.isEnabled() 87 enabled: Emailer.isEnabled()
71 }, 88 },
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index b898e64fa..7cbfa8569 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -75,6 +75,27 @@ export class PluginManager {
75 return registered 75 return registered
76 } 76 }
77 77
78 getRegisteredPlugins () {
79 return this.registeredPlugins
80 }
81
82 async runHook (hookName: string, param?: any) {
83 let result = param
84
85 const wait = hookName.startsWith('static:')
86
87 for (const hook of this.hooks[hookName]) {
88 try {
89 if (wait) result = await hook.handler(param)
90 else result = hook.handler()
91 } catch (err) {
92 logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err })
93 }
94 }
95
96 return result
97 }
98
78 async unregister (name: string) { 99 async unregister (name: string) {
79 const plugin = this.getRegisteredPlugin(name) 100 const plugin = this.getRegisteredPlugin(name)
80 101
diff --git a/shared/models/plugins/plugin-scope.type.ts b/shared/models/plugins/plugin-scope.type.ts
new file mode 100644
index 000000000..b63ae43ec
--- /dev/null
+++ b/shared/models/plugins/plugin-scope.type.ts
@@ -0,0 +1 @@
export type PluginScope = 'common' | 'video-watch'
diff --git a/shared/models/plugins/register.model.ts b/shared/models/plugins/register.model.ts
index 3817007ae..0ed2157bd 100644
--- a/shared/models/plugins/register.model.ts
+++ b/shared/models/plugins/register.model.ts
@@ -1,4 +1,4 @@
1export type RegisterHookOptions = { 1export interface RegisterHookOptions {
2 target: string 2 target: string
3 handler: Function 3 handler: Function
4 priority?: number 4 priority?: number
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index d937e9c05..c259a849a 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -1,4 +1,12 @@
1import { NSFWPolicyType } from '../videos/nsfw-policy.type' 1import { NSFWPolicyType } from '../videos/nsfw-policy.type'
2import { ClientScript } from '../plugins/plugin-package-json.model'
3
4export type ServerConfigPlugin = {
5 name: string
6 version: string
7 description: string
8 clientScripts: { [name: string]: ClientScript }
9}
2 10
3export interface ServerConfig { 11export interface ServerConfig {
4 serverVersion: string 12 serverVersion: string
@@ -16,6 +24,8 @@ export interface ServerConfig {
16 } 24 }
17 } 25 }
18 26
27 plugins: ServerConfigPlugin[]
28
19 email: { 29 email: {
20 enabled: boolean 30 enabled: boolean
21 } 31 }