aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-07-10 16:59:53 +0200
committerChocobozzz <chocobozzz@cpy.re>2019-07-24 10:58:16 +0200
commitad91e7006e41f8ee5b8dcefee30f99e8ca44133a (patch)
treed860f20e05b036fa1a96e049c74deffd7f5d2b00
parentffb321bedca46d6987c7b31dd58e5dea96ea2ea2 (diff)
downloadPeerTube-ad91e7006e41f8ee5b8dcefee30f99e8ca44133a.tar.gz
PeerTube-ad91e7006e41f8ee5b8dcefee30f99e8ca44133a.tar.zst
PeerTube-ad91e7006e41f8ee5b8dcefee30f99e8ca44133a.zip
WIP plugins: plugin settings on server side
-rw-r--r--server/controllers/api/index.ts2
-rw-r--r--server/controllers/api/plugins.ts121
-rw-r--r--server/helpers/custom-validators/video-channels.ts4
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/lib/plugins/plugin-manager.ts111
-rw-r--r--server/lib/plugins/yarn.ts8
-rw-r--r--server/middlewares/validators/plugins.ts89
-rw-r--r--server/middlewares/validators/sort.ts5
-rw-r--r--server/models/server/plugin.ts78
-rw-r--r--server/typings/express.ts4
-rw-r--r--shared/models/plugins/install-plugin.model.ts3
-rw-r--r--shared/models/plugins/manage-plugin.model.ts3
-rw-r--r--shared/models/plugins/peertube-plugin.model.ts12
-rw-r--r--shared/models/plugins/plugin-library.model.ts3
-rw-r--r--shared/models/plugins/plugin-settings-manager.model.ts7
-rw-r--r--shared/models/plugins/register-hook.model.ts (renamed from shared/models/plugins/register.model.ts)0
-rw-r--r--shared/models/plugins/register-options.model.ts11
-rw-r--r--shared/models/plugins/register-options.type.ts5
-rw-r--r--shared/models/plugins/register-setting.model.ts6
-rw-r--r--shared/models/users/user-right.enum.ts4
20 files changed, 419 insertions, 61 deletions
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index ea2615e28..0876283a2 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -14,6 +14,7 @@ import { searchRouter } from './search'
14import { overviewsRouter } from './overviews' 14import { overviewsRouter } from './overviews'
15import { videoPlaylistRouter } from './video-playlist' 15import { videoPlaylistRouter } from './video-playlist'
16import { CONFIG } from '../../initializers/config' 16import { CONFIG } from '../../initializers/config'
17import { pluginsRouter } from '../plugins'
17 18
18const apiRouter = express.Router() 19const apiRouter = express.Router()
19 20
@@ -42,6 +43,7 @@ apiRouter.use('/videos', videosRouter)
42apiRouter.use('/jobs', jobsRouter) 43apiRouter.use('/jobs', jobsRouter)
43apiRouter.use('/search', searchRouter) 44apiRouter.use('/search', searchRouter)
44apiRouter.use('/overviews', overviewsRouter) 45apiRouter.use('/overviews', overviewsRouter)
46apiRouter.use('/plugins', pluginsRouter)
45apiRouter.use('/ping', pong) 47apiRouter.use('/ping', pong)
46apiRouter.use('/*', badRequest) 48apiRouter.use('/*', badRequest)
47 49
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts
new file mode 100644
index 000000000..89cc67f54
--- /dev/null
+++ b/server/controllers/api/plugins.ts
@@ -0,0 +1,121 @@
1import * as express from 'express'
2import { getFormattedObjects } from '../../helpers/utils'
3import {
4 asyncMiddleware,
5 authenticate,
6 ensureUserHasRight,
7 paginationValidator,
8 setDefaultPagination,
9 setDefaultSort
10} from '../../middlewares'
11import { pluginsSortValidator } from '../../middlewares/validators'
12import { PluginModel } from '../../models/server/plugin'
13import { UserRight } from '../../../shared/models/users'
14import {
15 enabledPluginValidator,
16 installPluginValidator,
17 listPluginsValidator,
18 uninstallPluginValidator,
19 updatePluginSettingsValidator
20} from '../../middlewares/validators/plugins'
21import { PluginManager } from '../../lib/plugins/plugin-manager'
22import { InstallPlugin } from '../../../shared/models/plugins/install-plugin.model'
23import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model'
24
25const pluginRouter = express.Router()
26
27pluginRouter.get('/',
28 authenticate,
29 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
30 listPluginsValidator,
31 paginationValidator,
32 pluginsSortValidator,
33 setDefaultSort,
34 setDefaultPagination,
35 asyncMiddleware(listPlugins)
36)
37
38pluginRouter.get('/:pluginName/settings',
39 authenticate,
40 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
41 asyncMiddleware(enabledPluginValidator),
42 asyncMiddleware(listPluginSettings)
43)
44
45pluginRouter.put('/:pluginName/settings',
46 authenticate,
47 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
48 updatePluginSettingsValidator,
49 asyncMiddleware(enabledPluginValidator),
50 asyncMiddleware(updatePluginSettings)
51)
52
53pluginRouter.post('/install',
54 authenticate,
55 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
56 installPluginValidator,
57 asyncMiddleware(installPlugin)
58)
59
60pluginRouter.post('/uninstall',
61 authenticate,
62 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
63 uninstallPluginValidator,
64 asyncMiddleware(uninstallPlugin)
65)
66
67// ---------------------------------------------------------------------------
68
69export {
70 pluginRouter
71}
72
73// ---------------------------------------------------------------------------
74
75async function listPlugins (req: express.Request, res: express.Response) {
76 const type = req.query.type
77
78 const resultList = await PluginModel.listForApi({
79 type,
80 start: req.query.start,
81 count: req.query.count,
82 sort: req.query.sort
83 })
84
85 return res.json(getFormattedObjects(resultList.data, resultList.total))
86}
87
88async function installPlugin (req: express.Request, res: express.Response) {
89 const body: InstallPlugin = req.body
90
91 await PluginManager.Instance.install(body.npmName)
92
93 return res.sendStatus(204)
94}
95
96async function uninstallPlugin (req: express.Request, res: express.Response) {
97 const body: ManagePlugin = req.body
98
99 await PluginManager.Instance.uninstall(body.npmName)
100
101 return res.sendStatus(204)
102}
103
104async function listPluginSettings (req: express.Request, res: express.Response) {
105 const plugin = res.locals.plugin
106
107 const settings = await PluginManager.Instance.getSettings(plugin.name)
108
109 return res.json({
110 settings
111 })
112}
113
114async function updatePluginSettings (req: express.Request, res: express.Response) {
115 const plugin = res.locals.plugin
116
117 plugin.settings = req.body.settings
118 await plugin.save()
119
120 return res.sendStatus(204)
121}
diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts
index f818ce8f1..e1a2f9503 100644
--- a/server/helpers/custom-validators/video-channels.ts
+++ b/server/helpers/custom-validators/video-channels.ts
@@ -51,9 +51,7 @@ export {
51 51
52function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) { 52function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) {
53 if (!videoChannel) { 53 if (!videoChannel) {
54 res.status(404) 54 ``
55 .json({ error: 'Video channel not found' })
56 .end()
57 55
58 return false 56 return false
59 } 57 }
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index e5f88b71d..4163fe49d 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -62,7 +62,9 @@ const SORTABLE_COLUMNS = {
62 62
63 USER_NOTIFICATIONS: [ 'createdAt' ], 63 USER_NOTIFICATIONS: [ 'createdAt' ],
64 64
65 VIDEO_PLAYLISTS: [ 'displayName', 'createdAt', 'updatedAt' ] 65 VIDEO_PLAYLISTS: [ 'displayName', 'createdAt', 'updatedAt' ],
66
67 PLUGINS: [ 'name', 'createdAt', 'updatedAt' ]
66} 68}
67 69
68const OAUTH_LIFETIME = { 70const OAUTH_LIFETIME = {
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 8496979f8..3d8375acd 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -1,6 +1,5 @@
1import { PluginModel } from '../../models/server/plugin' 1import { PluginModel } from '../../models/server/plugin'
2import { logger } from '../../helpers/logger' 2import { logger } from '../../helpers/logger'
3import { RegisterHookOptions } from '../../../shared/models/plugins/register.model'
4import { basename, join } from 'path' 3import { basename, join } from 'path'
5import { CONFIG } from '../../initializers/config' 4import { CONFIG } from '../../initializers/config'
6import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' 5import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins'
@@ -11,7 +10,9 @@ import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
11import { PluginType } from '../../../shared/models/plugins/plugin.type' 10import { PluginType } from '../../../shared/models/plugins/plugin.type'
12import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' 11import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
13import { outputFile } from 'fs-extra' 12import { outputFile } from 'fs-extra'
14import { ServerConfigPlugin } from '../../../shared/models/server' 13import { RegisterSettingOptions } from '../../../shared/models/plugins/register-setting.model'
14import { RegisterHookOptions } from '../../../shared/models/plugins/register-hook.model'
15import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model'
15 16
16export interface RegisteredPlugin { 17export interface RegisteredPlugin {
17 name: string 18 name: string
@@ -43,26 +44,13 @@ export class PluginManager {
43 private static instance: PluginManager 44 private static instance: PluginManager
44 45
45 private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {} 46 private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {}
47 private settings: { [ name: string ]: RegisterSettingOptions[] } = {}
46 private hooks: { [ name: string ]: HookInformationValue[] } = {} 48 private hooks: { [ name: string ]: HookInformationValue[] } = {}
47 49
48 private constructor () { 50 private constructor () {
49 } 51 }
50 52
51 async registerPluginsAndThemes () { 53 // ###################### Getters ######################
52 await this.resetCSSGlobalFile()
53
54 const plugins = await PluginModel.listEnabledPluginsAndThemes()
55
56 for (const plugin of plugins) {
57 try {
58 await this.registerPluginOrTheme(plugin)
59 } catch (err) {
60 logger.error('Cannot register plugin %s, skipping.', plugin.name, { err })
61 }
62 }
63
64 this.sortHooksByPriority()
65 }
66 54
67 getRegisteredPluginOrTheme (name: string) { 55 getRegisteredPluginOrTheme (name: string) {
68 return this.registeredPlugins[name] 56 return this.registeredPlugins[name]
@@ -92,6 +80,12 @@ export class PluginManager {
92 return this.getRegisteredPluginsOrThemes(PluginType.THEME) 80 return this.getRegisteredPluginsOrThemes(PluginType.THEME)
93 } 81 }
94 82
83 getSettings (name: string) {
84 return this.settings[name] || []
85 }
86
87 // ###################### Hooks ######################
88
95 async runHook (hookName: string, param?: any) { 89 async runHook (hookName: string, param?: any) {
96 let result = param 90 let result = param
97 91
@@ -99,8 +93,11 @@ export class PluginManager {
99 93
100 for (const hook of this.hooks[hookName]) { 94 for (const hook of this.hooks[hookName]) {
101 try { 95 try {
102 if (wait) result = await hook.handler(param) 96 if (wait) {
103 else result = hook.handler() 97 result = await hook.handler(param)
98 } else {
99 result = hook.handler()
100 }
104 } catch (err) { 101 } catch (err) {
105 logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err }) 102 logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err })
106 } 103 }
@@ -109,6 +106,24 @@ export class PluginManager {
109 return result 106 return result
110 } 107 }
111 108
109 // ###################### Registration ######################
110
111 async registerPluginsAndThemes () {
112 await this.resetCSSGlobalFile()
113
114 const plugins = await PluginModel.listEnabledPluginsAndThemes()
115
116 for (const plugin of plugins) {
117 try {
118 await this.registerPluginOrTheme(plugin)
119 } catch (err) {
120 logger.error('Cannot register plugin %s, skipping.', plugin.name, { err })
121 }
122 }
123
124 this.sortHooksByPriority()
125 }
126
112 async unregister (name: string) { 127 async unregister (name: string) {
113 const plugin = this.getRegisteredPlugin(name) 128 const plugin = this.getRegisteredPlugin(name)
114 129
@@ -133,7 +148,9 @@ export class PluginManager {
133 await this.regeneratePluginGlobalCSS() 148 await this.regeneratePluginGlobalCSS()
134 } 149 }
135 150
136 async install (toInstall: string, version: string, fromDisk = false) { 151 // ###################### Installation ######################
152
153 async install (toInstall: string, version?: string, fromDisk = false) {
137 let plugin: PluginModel 154 let plugin: PluginModel
138 let name: string 155 let name: string
139 156
@@ -206,6 +223,8 @@ export class PluginManager {
206 logger.info('Plugin %s uninstalled.', packageName) 223 logger.info('Plugin %s uninstalled.', packageName)
207 } 224 }
208 225
226 // ###################### Private register ######################
227
209 private async registerPluginOrTheme (plugin: PluginModel) { 228 private async registerPluginOrTheme (plugin: PluginModel) {
210 logger.info('Registering plugin or theme %s.', plugin.name) 229 logger.info('Registering plugin or theme %s.', plugin.name)
211 230
@@ -251,13 +270,25 @@ export class PluginManager {
251 }) 270 })
252 } 271 }
253 272
273 const registerSetting = (options: RegisterSettingOptions) => {
274 if (!this.settings[plugin.name]) this.settings[plugin.name] = []
275
276 this.settings[plugin.name].push(options)
277 }
278
279 const settingsManager: PluginSettingsManager = {
280 getSetting: (name: string) => PluginModel.getSetting(plugin.name, name),
281
282 setSetting: (name: string, value: string) => PluginModel.setSetting(plugin.name, name, value)
283 }
284
254 const library: PluginLibrary = require(join(pluginPath, packageJSON.library)) 285 const library: PluginLibrary = require(join(pluginPath, packageJSON.library))
255 286
256 if (!isLibraryCodeValid(library)) { 287 if (!isLibraryCodeValid(library)) {
257 throw new Error('Library code is not valid (miss register or unregister function)') 288 throw new Error('Library code is not valid (miss register or unregister function)')
258 } 289 }
259 290
260 library.register({ registerHook }) 291 library.register({ registerHook, registerSetting, settingsManager })
261 292
262 logger.info('Add plugin %s CSS to global file.', plugin.name) 293 logger.info('Add plugin %s CSS to global file.', plugin.name)
263 294
@@ -266,13 +297,7 @@ export class PluginManager {
266 return library 297 return library
267 } 298 }
268 299
269 private sortHooksByPriority () { 300 // ###################### CSS ######################
270 for (const hookName of Object.keys(this.hooks)) {
271 this.hooks[hookName].sort((a, b) => {
272 return b.priority - a.priority
273 })
274 }
275 }
276 301
277 private resetCSSGlobalFile () { 302 private resetCSSGlobalFile () {
278 return outputFile(PLUGIN_GLOBAL_CSS_PATH, '') 303 return outputFile(PLUGIN_GLOBAL_CSS_PATH, '')
@@ -296,6 +321,26 @@ export class PluginManager {
296 }) 321 })
297 } 322 }
298 323
324 private async regeneratePluginGlobalCSS () {
325 await this.resetCSSGlobalFile()
326
327 for (const key of Object.keys(this.registeredPlugins)) {
328 const plugin = this.registeredPlugins[key]
329
330 await this.addCSSToGlobalFile(plugin.path, plugin.css)
331 }
332 }
333
334 // ###################### Utils ######################
335
336 private sortHooksByPriority () {
337 for (const hookName of Object.keys(this.hooks)) {
338 this.hooks[hookName].sort((a, b) => {
339 return b.priority - a.priority
340 })
341 }
342 }
343
299 private getPackageJSON (pluginName: string, pluginType: PluginType) { 344 private getPackageJSON (pluginName: string, pluginType: PluginType) {
300 const pluginPath = join(this.getPluginPath(pluginName, pluginType), 'package.json') 345 const pluginPath = join(this.getPluginPath(pluginName, pluginType), 'package.json')
301 346
@@ -312,15 +357,7 @@ export class PluginManager {
312 return name.replace(/^peertube-((theme)|(plugin))-/, '') 357 return name.replace(/^peertube-((theme)|(plugin))-/, '')
313 } 358 }
314 359
315 private async regeneratePluginGlobalCSS () { 360 // ###################### Private getters ######################
316 await this.resetCSSGlobalFile()
317
318 for (const key of Object.keys(this.registeredPlugins)) {
319 const plugin = this.registeredPlugins[key]
320
321 await this.addCSSToGlobalFile(plugin.path, plugin.css)
322 }
323 }
324 361
325 private getRegisteredPluginsOrThemes (type: PluginType) { 362 private getRegisteredPluginsOrThemes (type: PluginType) {
326 const plugins: RegisteredPlugin[] = [] 363 const plugins: RegisteredPlugin[] = []
diff --git a/server/lib/plugins/yarn.ts b/server/lib/plugins/yarn.ts
index 35fe1625f..5fe1c5046 100644
--- a/server/lib/plugins/yarn.ts
+++ b/server/lib/plugins/yarn.ts
@@ -5,12 +5,14 @@ import { CONFIG } from '../../initializers/config'
5import { outputJSON, pathExists } from 'fs-extra' 5import { outputJSON, pathExists } from 'fs-extra'
6import { join } from 'path' 6import { join } from 'path'
7 7
8async function installNpmPlugin (name: string, version: string) { 8async function installNpmPlugin (name: string, version?: string) {
9 // Security check 9 // Security check
10 checkNpmPluginNameOrThrow(name) 10 checkNpmPluginNameOrThrow(name)
11 checkPluginVersionOrThrow(version) 11 if (version) checkPluginVersionOrThrow(version)
12
13 let toInstall = name
14 if (version) toInstall += `@${version}`
12 15
13 const toInstall = `${name}@${version}`
14 await execYarn('add ' + toInstall) 16 await execYarn('add ' + toInstall)
15} 17}
16 18
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts
index fcb461624..265ac7c17 100644
--- a/server/middlewares/validators/plugins.ts
+++ b/server/middlewares/validators/plugins.ts
@@ -1,10 +1,11 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param } from 'express-validator/check' 2import { param, query, body } from 'express-validator/check'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils' 4import { areValidationErrors } from './utils'
5import { isPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' 5import { isPluginNameValid, isPluginTypeValid, isPluginVersionValid, isNpmPluginNameValid } from '../../helpers/custom-validators/plugins'
6import { PluginManager } from '../../lib/plugins/plugin-manager' 6import { PluginManager } from '../../lib/plugins/plugin-manager'
7import { isSafePath } from '../../helpers/custom-validators/misc' 7import { isBooleanValid, isSafePath } from '../../helpers/custom-validators/misc'
8import { PluginModel } from '../../models/server/plugin'
8 9
9const servePluginStaticDirectoryValidator = [ 10const servePluginStaticDirectoryValidator = [
10 param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'), 11 param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'),
@@ -28,8 +29,88 @@ const servePluginStaticDirectoryValidator = [
28 } 29 }
29] 30]
30 31
32const listPluginsValidator = [
33 query('type')
34 .optional()
35 .custom(isPluginTypeValid).withMessage('Should have a valid plugin type'),
36 query('uninstalled')
37 .optional()
38 .toBoolean()
39 .custom(isBooleanValid).withMessage('Should have a valid uninstalled attribute'),
40
41 (req: express.Request, res: express.Response, next: express.NextFunction) => {
42 logger.debug('Checking listPluginsValidator parameters', { parameters: req.query })
43
44 if (areValidationErrors(req, res)) return
45
46 return next()
47 }
48]
49
50const installPluginValidator = [
51 body('npmName').custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'),
52
53 (req: express.Request, res: express.Response, next: express.NextFunction) => {
54 logger.debug('Checking installPluginValidator parameters', { parameters: req.body })
55
56 if (areValidationErrors(req, res)) return
57
58 return next()
59 }
60]
61
62const uninstallPluginValidator = [
63 body('npmName').custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'),
64
65 (req: express.Request, res: express.Response, next: express.NextFunction) => {
66 logger.debug('Checking managePluginValidator parameters', { parameters: req.body })
67
68 if (areValidationErrors(req, res)) return
69
70 return next()
71 }
72]
73
74const enabledPluginValidator = [
75 body('name').custom(isPluginNameValid).withMessage('Should have a valid plugin name'),
76
77 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
78 logger.debug('Checking enabledPluginValidator parameters', { parameters: req.body })
79
80 if (areValidationErrors(req, res)) return
81
82 const plugin = await PluginModel.load(req.body.name)
83 if (!plugin) {
84 return res.status(404)
85 .json({ error: 'Plugin not found' })
86 .end()
87 }
88
89 res.locals.plugin = plugin
90
91 return next()
92 }
93]
94
95const updatePluginSettingsValidator = [
96 body('settings').exists().withMessage('Should have settings'),
97
98 (req: express.Request, res: express.Response, next: express.NextFunction) => {
99 logger.debug('Checking enabledPluginValidator parameters', { parameters: req.body })
100
101 if (areValidationErrors(req, res)) return
102
103 return next()
104 }
105]
106
31// --------------------------------------------------------------------------- 107// ---------------------------------------------------------------------------
32 108
33export { 109export {
34 servePluginStaticDirectoryValidator 110 servePluginStaticDirectoryValidator,
111 updatePluginSettingsValidator,
112 uninstallPluginValidator,
113 enabledPluginValidator,
114 installPluginValidator,
115 listPluginsValidator
35} 116}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index b497798d1..102db85cb 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -21,6 +21,7 @@ const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUM
21const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) 21const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
22const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS) 22const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS)
23const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS) 23const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS)
24const SORTABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PLUGINS)
24 25
25const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 26const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
26const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 27const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
@@ -41,6 +42,7 @@ const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COL
41const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS) 42const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS)
42const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS) 43const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS)
43const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS) 44const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS)
45const pluginsSortValidator = checkSort(SORTABLE_PLUGINS_COLUMNS)
44 46
45// --------------------------------------------------------------------------- 47// ---------------------------------------------------------------------------
46 48
@@ -63,5 +65,6 @@ export {
63 accountsBlocklistSortValidator, 65 accountsBlocklistSortValidator,
64 serversBlocklistSortValidator, 66 serversBlocklistSortValidator,
65 userNotificationsSortValidator, 67 userNotificationsSortValidator,
66 videoPlaylistsSortValidator 68 videoPlaylistsSortValidator,
69 pluginsSortValidator
67} 70}
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts
index b3b8276df..059a442de 100644
--- a/server/models/server/plugin.ts
+++ b/server/models/server/plugin.ts
@@ -1,11 +1,20 @@
1import { AllowNull, Column, CreatedAt, DataType, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { throwIfNotValid } from '../utils' 2import { getSort, throwIfNotValid } from '../utils'
3import { 3import {
4 isPluginDescriptionValid, 4 isPluginDescriptionValid,
5 isPluginNameValid, 5 isPluginNameValid,
6 isPluginTypeValid, 6 isPluginTypeValid,
7 isPluginVersionValid 7 isPluginVersionValid
8} from '../../helpers/custom-validators/plugins' 8} from '../../helpers/custom-validators/plugins'
9import { PluginType } from '../../../shared/models/plugins/plugin.type'
10import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model'
11import { FindAndCountOptions } from 'sequelize'
12
13@DefaultScope(() => ({
14 attributes: {
15 exclude: [ 'storage' ]
16 }
17}))
9 18
10@Table({ 19@Table({
11 tableName: 'plugin', 20 tableName: 'plugin',
@@ -85,14 +94,75 @@ export class PluginModel extends Model<PluginModel> {
85 return PluginModel.findOne(query) 94 return PluginModel.findOne(query)
86 } 95 }
87 96
88 static uninstall (pluginName: string) { 97 static getSetting (pluginName: string, settingName: string) {
98 const query = {
99 attributes: [ 'settings' ],
100 where: {
101 name: pluginName
102 }
103 }
104
105 return PluginModel.findOne(query)
106 .then(p => p.settings)
107 .then(settings => {
108 if (!settings) return undefined
109
110 return settings[settingName]
111 })
112 }
113
114 static setSetting (pluginName: string, settingName: string, settingValue: string) {
89 const query = { 115 const query = {
90 where: { 116 where: {
91 name: pluginName 117 name: pluginName
92 } 118 }
93 } 119 }
94 120
95 return PluginModel.update({ enabled: false, uninstalled: true }, query) 121 const toSave = {
122 [`settings.${settingName}`]: settingValue
123 }
124
125 return PluginModel.update(toSave, query)
126 .then(() => undefined)
127 }
128
129 static listForApi (options: {
130 type?: PluginType,
131 uninstalled?: boolean,
132 start: number,
133 count: number,
134 sort: string
135 }) {
136 const query: FindAndCountOptions = {
137 offset: options.start,
138 limit: options.count,
139 order: getSort(options.sort),
140 where: {}
141 }
142
143 if (options.type) query.where['type'] = options.type
144 if (options.uninstalled) query.where['uninstalled'] = options.uninstalled
145
146 return PluginModel
147 .findAndCountAll(query)
148 .then(({ rows, count }) => {
149 return { total: count, data: rows }
150 })
151 }
152
153 toFormattedJSON (): PeerTubePlugin {
154 return {
155 name: this.name,
156 type: this.type,
157 version: this.version,
158 enabled: this.enabled,
159 uninstalled: this.uninstalled,
160 peertubeEngine: this.peertubeEngine,
161 description: this.description,
162 settings: this.settings,
163 createdAt: this.createdAt,
164 updatedAt: this.updatedAt
165 }
96 } 166 }
97 167
98} 168}
diff --git a/server/typings/express.ts b/server/typings/express.ts
index aec10b606..3bffc1e9a 100644
--- a/server/typings/express.ts
+++ b/server/typings/express.ts
@@ -21,10 +21,10 @@ import { VideoBlacklistModel } from '../models/video/video-blacklist'
21import { VideoCaptionModel } from '../models/video/video-caption' 21import { VideoCaptionModel } from '../models/video/video-caption'
22import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' 22import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
23import { RegisteredPlugin } from '../lib/plugins/plugin-manager' 23import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
24import { PluginModel } from '../models/server/plugin'
24 25
25declare module 'express' { 26declare module 'express' {
26 27
27
28 interface Response { 28 interface Response {
29 locals: { 29 locals: {
30 video?: VideoModel 30 video?: VideoModel
@@ -81,6 +81,8 @@ declare module 'express' {
81 authenticated?: boolean 81 authenticated?: boolean
82 82
83 registeredPlugin?: RegisteredPlugin 83 registeredPlugin?: RegisteredPlugin
84
85 plugin?: PluginModel
84 } 86 }
85 } 87 }
86} 88}
diff --git a/shared/models/plugins/install-plugin.model.ts b/shared/models/plugins/install-plugin.model.ts
new file mode 100644
index 000000000..03d87fe57
--- /dev/null
+++ b/shared/models/plugins/install-plugin.model.ts
@@ -0,0 +1,3 @@
1export interface InstallPlugin {
2 npmName: string
3}
diff --git a/shared/models/plugins/manage-plugin.model.ts b/shared/models/plugins/manage-plugin.model.ts
new file mode 100644
index 000000000..612b3056c
--- /dev/null
+++ b/shared/models/plugins/manage-plugin.model.ts
@@ -0,0 +1,3 @@
1export interface ManagePlugin {
2 npmName: string
3}
diff --git a/shared/models/plugins/peertube-plugin.model.ts b/shared/models/plugins/peertube-plugin.model.ts
new file mode 100644
index 000000000..2a1dfb3a7
--- /dev/null
+++ b/shared/models/plugins/peertube-plugin.model.ts
@@ -0,0 +1,12 @@
1export interface PeerTubePlugin {
2 name: string
3 type: number
4 version: string
5 enabled: boolean
6 uninstalled: boolean
7 peertubeEngine: string
8 description: string
9 settings: any
10 createdAt: Date
11 updatedAt: Date
12}
diff --git a/shared/models/plugins/plugin-library.model.ts b/shared/models/plugins/plugin-library.model.ts
index 8eb18d720..df6499b6b 100644
--- a/shared/models/plugins/plugin-library.model.ts
+++ b/shared/models/plugins/plugin-library.model.ts
@@ -1,6 +1,7 @@
1import { RegisterOptions } from './register-options.type' 1import { RegisterOptions } from './register-options.model'
2 2
3export interface PluginLibrary { 3export interface PluginLibrary {
4 register: (options: RegisterOptions) => void 4 register: (options: RegisterOptions) => void
5
5 unregister: () => Promise<any> 6 unregister: () => Promise<any>
6} 7}
diff --git a/shared/models/plugins/plugin-settings-manager.model.ts b/shared/models/plugins/plugin-settings-manager.model.ts
new file mode 100644
index 000000000..63390a190
--- /dev/null
+++ b/shared/models/plugins/plugin-settings-manager.model.ts
@@ -0,0 +1,7 @@
1import * as Bluebird from 'bluebird'
2
3export interface PluginSettingsManager {
4 getSetting: (name: string) => Bluebird<string>
5
6 setSetting: (name: string, value: string) => Bluebird<any>
7}
diff --git a/shared/models/plugins/register.model.ts b/shared/models/plugins/register-hook.model.ts
index 0ed2157bd..0ed2157bd 100644
--- a/shared/models/plugins/register.model.ts
+++ b/shared/models/plugins/register-hook.model.ts
diff --git a/shared/models/plugins/register-options.model.ts b/shared/models/plugins/register-options.model.ts
new file mode 100644
index 000000000..e60ce3fe0
--- /dev/null
+++ b/shared/models/plugins/register-options.model.ts
@@ -0,0 +1,11 @@
1import { RegisterHookOptions } from './register-hook.model'
2import { RegisterSettingOptions } from './register-setting.model'
3import { PluginSettingsManager } from './plugin-settings-manager.model'
4
5export type RegisterOptions = {
6 registerHook: (options: RegisterHookOptions) => void
7
8 registerSetting: (options: RegisterSettingOptions) => void
9
10 settingsManager: PluginSettingsManager
11}
diff --git a/shared/models/plugins/register-options.type.ts b/shared/models/plugins/register-options.type.ts
deleted file mode 100644
index a074f3931..000000000
--- a/shared/models/plugins/register-options.type.ts
+++ /dev/null
@@ -1,5 +0,0 @@
1import { RegisterHookOptions } from './register.model'
2
3export type RegisterOptions = {
4 registerHook: (options: RegisterHookOptions) => void
5}
diff --git a/shared/models/plugins/register-setting.model.ts b/shared/models/plugins/register-setting.model.ts
new file mode 100644
index 000000000..e7af75dca
--- /dev/null
+++ b/shared/models/plugins/register-setting.model.ts
@@ -0,0 +1,6 @@
1export interface RegisterSettingOptions {
2 name: string
3 label: string
4 type: 'input'
5 default?: string
6}
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts
index 71701bdb4..4a28a229d 100644
--- a/shared/models/users/user-right.enum.ts
+++ b/shared/models/users/user-right.enum.ts
@@ -31,5 +31,7 @@ export enum UserRight {
31 UPDATE_ANY_VIDEO_PLAYLIST, 31 UPDATE_ANY_VIDEO_PLAYLIST,
32 32
33 SEE_ALL_VIDEOS, 33 SEE_ALL_VIDEOS,
34 CHANGE_VIDEO_OWNERSHIP 34 CHANGE_VIDEO_OWNERSHIP,
35
36 MANAGE_PLUGINS
35} 37}