diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/plugins.ts | 38 | ||||
-rw-r--r-- | server/controllers/index.ts | 1 | ||||
-rw-r--r-- | server/controllers/plugins.ts | 40 | ||||
-rw-r--r-- | server/lib/plugins/plugin-manager.ts | 102 | ||||
-rw-r--r-- | server/middlewares/validators/plugins.ts | 20 | ||||
-rw-r--r-- | server/models/server/plugin.ts | 32 | ||||
-rw-r--r-- | server/tools/peertube-plugins.ts | 44 |
7 files changed, 202 insertions, 75 deletions
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts index 8e59f27cf..14675fdf3 100644 --- a/server/controllers/api/plugins.ts +++ b/server/controllers/api/plugins.ts | |||
@@ -13,13 +13,13 @@ import { PluginModel } from '../../models/server/plugin' | |||
13 | import { UserRight } from '../../../shared/models/users' | 13 | import { UserRight } from '../../../shared/models/users' |
14 | import { | 14 | import { |
15 | existingPluginValidator, | 15 | existingPluginValidator, |
16 | installPluginValidator, | 16 | installOrUpdatePluginValidator, |
17 | listPluginsValidator, | 17 | listPluginsValidator, |
18 | uninstallPluginValidator, | 18 | uninstallPluginValidator, |
19 | updatePluginSettingsValidator | 19 | updatePluginSettingsValidator |
20 | } from '../../middlewares/validators/plugins' | 20 | } from '../../middlewares/validators/plugins' |
21 | import { PluginManager } from '../../lib/plugins/plugin-manager' | 21 | import { PluginManager } from '../../lib/plugins/plugin-manager' |
22 | import { InstallPlugin } from '../../../shared/models/plugins/install-plugin.model' | 22 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' |
23 | import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model' | 23 | import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model' |
24 | import { logger } from '../../helpers/logger' | 24 | import { logger } from '../../helpers/logger' |
25 | 25 | ||
@@ -61,10 +61,17 @@ pluginRouter.put('/:npmName/settings', | |||
61 | pluginRouter.post('/install', | 61 | pluginRouter.post('/install', |
62 | authenticate, | 62 | authenticate, |
63 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), | 63 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), |
64 | installPluginValidator, | 64 | installOrUpdatePluginValidator, |
65 | asyncMiddleware(installPlugin) | 65 | asyncMiddleware(installPlugin) |
66 | ) | 66 | ) |
67 | 67 | ||
68 | pluginRouter.post('/update', | ||
69 | authenticate, | ||
70 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), | ||
71 | installOrUpdatePluginValidator, | ||
72 | asyncMiddleware(updatePlugin) | ||
73 | ) | ||
74 | |||
68 | pluginRouter.post('/uninstall', | 75 | pluginRouter.post('/uninstall', |
69 | authenticate, | 76 | authenticate, |
70 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), | 77 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), |
@@ -100,18 +107,33 @@ function getPlugin (req: express.Request, res: express.Response) { | |||
100 | } | 107 | } |
101 | 108 | ||
102 | async function installPlugin (req: express.Request, res: express.Response) { | 109 | async function installPlugin (req: express.Request, res: express.Response) { |
103 | const body: InstallPlugin = req.body | 110 | const body: InstallOrUpdatePlugin = req.body |
104 | 111 | ||
105 | const fromDisk = !!body.path | 112 | const fromDisk = !!body.path |
106 | const toInstall = body.npmName || body.path | 113 | const toInstall = body.npmName || body.path |
107 | try { | 114 | try { |
108 | await PluginManager.Instance.install(toInstall, undefined, fromDisk) | 115 | const plugin = await PluginManager.Instance.install(toInstall, undefined, fromDisk) |
116 | |||
117 | return res.json(plugin.toFormattedJSON()) | ||
109 | } catch (err) { | 118 | } catch (err) { |
110 | logger.warn('Cannot install plugin %s.', toInstall, { err }) | 119 | logger.warn('Cannot install plugin %s.', toInstall, { err }) |
111 | return res.sendStatus(400) | 120 | return res.sendStatus(400) |
112 | } | 121 | } |
122 | } | ||
113 | 123 | ||
114 | return res.sendStatus(204) | 124 | async function updatePlugin (req: express.Request, res: express.Response) { |
125 | const body: InstallOrUpdatePlugin = req.body | ||
126 | |||
127 | const fromDisk = !!body.path | ||
128 | const toUpdate = body.npmName || body.path | ||
129 | try { | ||
130 | const plugin = await PluginManager.Instance.update(toUpdate, undefined, fromDisk) | ||
131 | |||
132 | return res.json(plugin.toFormattedJSON()) | ||
133 | } catch (err) { | ||
134 | logger.warn('Cannot update plugin %s.', toUpdate, { err }) | ||
135 | return res.sendStatus(400) | ||
136 | } | ||
115 | } | 137 | } |
116 | 138 | ||
117 | async function uninstallPlugin (req: express.Request, res: express.Response) { | 139 | async function uninstallPlugin (req: express.Request, res: express.Response) { |
@@ -123,9 +145,7 @@ async function uninstallPlugin (req: express.Request, res: express.Response) { | |||
123 | } | 145 | } |
124 | 146 | ||
125 | function getPluginRegisteredSettings (req: express.Request, res: express.Response) { | 147 | function getPluginRegisteredSettings (req: express.Request, res: express.Response) { |
126 | const plugin = res.locals.plugin | 148 | const settings = PluginManager.Instance.getRegisteredSettings(req.params.npmName) |
127 | |||
128 | const settings = PluginManager.Instance.getSettings(plugin.name) | ||
129 | 149 | ||
130 | return res.json({ | 150 | return res.json({ |
131 | settings | 151 | settings |
diff --git a/server/controllers/index.ts b/server/controllers/index.ts index 869546dc7..8b3501712 100644 --- a/server/controllers/index.ts +++ b/server/controllers/index.ts | |||
@@ -8,4 +8,3 @@ export * from './webfinger' | |||
8 | export * from './tracker' | 8 | export * from './tracker' |
9 | export * from './bots' | 9 | export * from './bots' |
10 | export * from './plugins' | 10 | export * from './plugins' |
11 | export * from './themes' | ||
diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts index 05f03324d..f255d13e8 100644 --- a/server/controllers/plugins.ts +++ b/server/controllers/plugins.ts | |||
@@ -1,25 +1,42 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' | 2 | import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' |
3 | import { basename, join } from 'path' | 3 | import { join } from 'path' |
4 | import { RegisteredPlugin } from '../lib/plugins/plugin-manager' | 4 | import { RegisteredPlugin } from '../lib/plugins/plugin-manager' |
5 | import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins' | 5 | import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins' |
6 | import { serveThemeCSSValidator } from '../middlewares/validators/themes' | ||
7 | import { PluginType } from '../../shared/models/plugins/plugin.type' | ||
6 | 8 | ||
7 | const pluginsRouter = express.Router() | 9 | const pluginsRouter = express.Router() |
8 | 10 | ||
9 | pluginsRouter.get('/global.css', | 11 | pluginsRouter.get('/plugins/global.css', |
10 | servePluginGlobalCSS | 12 | servePluginGlobalCSS |
11 | ) | 13 | ) |
12 | 14 | ||
13 | pluginsRouter.get('/:pluginName/:pluginVersion/static/:staticEndpoint(*)', | 15 | pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)', |
14 | servePluginStaticDirectoryValidator, | 16 | servePluginStaticDirectoryValidator(PluginType.PLUGIN), |
15 | servePluginStaticDirectory | 17 | servePluginStaticDirectory |
16 | ) | 18 | ) |
17 | 19 | ||
18 | pluginsRouter.get('/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)', | 20 | pluginsRouter.get('/plugins/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)', |
19 | servePluginStaticDirectoryValidator, | 21 | servePluginStaticDirectoryValidator(PluginType.PLUGIN), |
20 | servePluginClientScripts | 22 | servePluginClientScripts |
21 | ) | 23 | ) |
22 | 24 | ||
25 | pluginsRouter.get('/themes/:pluginName/:pluginVersion/static/:staticEndpoint(*)', | ||
26 | servePluginStaticDirectoryValidator(PluginType.THEME), | ||
27 | servePluginStaticDirectory | ||
28 | ) | ||
29 | |||
30 | pluginsRouter.get('/themes/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)', | ||
31 | servePluginStaticDirectoryValidator(PluginType.THEME), | ||
32 | servePluginClientScripts | ||
33 | ) | ||
34 | |||
35 | pluginsRouter.get('/themes/:themeName/:themeVersion/css/:staticEndpoint(*)', | ||
36 | serveThemeCSSValidator, | ||
37 | serveThemeCSSDirectory | ||
38 | ) | ||
39 | |||
23 | // --------------------------------------------------------------------------- | 40 | // --------------------------------------------------------------------------- |
24 | 41 | ||
25 | export { | 42 | export { |
@@ -58,3 +75,14 @@ function servePluginClientScripts (req: express.Request, res: express.Response) | |||
58 | 75 | ||
59 | return res.sendFile(join(plugin.path, staticEndpoint)) | 76 | return res.sendFile(join(plugin.path, staticEndpoint)) |
60 | } | 77 | } |
78 | |||
79 | function serveThemeCSSDirectory (req: express.Request, res: express.Response) { | ||
80 | const plugin: RegisteredPlugin = res.locals.registeredPlugin | ||
81 | const staticEndpoint = req.params.staticEndpoint | ||
82 | |||
83 | if (plugin.css.includes(staticEndpoint) === false) { | ||
84 | return res.sendStatus(404) | ||
85 | } | ||
86 | |||
87 | return res.sendFile(join(plugin.path, staticEndpoint)) | ||
88 | } | ||
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index 8cdeff446..2fa80e878 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts | |||
@@ -15,6 +15,7 @@ import { RegisterHookOptions } from '../../../shared/models/plugins/register-hoo | |||
15 | import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model' | 15 | import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model' |
16 | 16 | ||
17 | export interface RegisteredPlugin { | 17 | export interface RegisteredPlugin { |
18 | npmName: string | ||
18 | name: string | 19 | name: string |
19 | version: string | 20 | version: string |
20 | description: string | 21 | description: string |
@@ -34,6 +35,7 @@ export interface RegisteredPlugin { | |||
34 | } | 35 | } |
35 | 36 | ||
36 | export interface HookInformationValue { | 37 | export interface HookInformationValue { |
38 | npmName: string | ||
37 | pluginName: string | 39 | pluginName: string |
38 | handler: Function | 40 | handler: Function |
39 | priority: number | 41 | priority: number |
@@ -52,12 +54,13 @@ export class PluginManager { | |||
52 | 54 | ||
53 | // ###################### Getters ###################### | 55 | // ###################### Getters ###################### |
54 | 56 | ||
55 | getRegisteredPluginOrTheme (name: string) { | 57 | getRegisteredPluginOrTheme (npmName: string) { |
56 | return this.registeredPlugins[name] | 58 | return this.registeredPlugins[npmName] |
57 | } | 59 | } |
58 | 60 | ||
59 | getRegisteredPlugin (name: string) { | 61 | getRegisteredPlugin (name: string) { |
60 | const registered = this.getRegisteredPluginOrTheme(name) | 62 | const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN) |
63 | const registered = this.getRegisteredPluginOrTheme(npmName) | ||
61 | 64 | ||
62 | if (!registered || registered.type !== PluginType.PLUGIN) return undefined | 65 | if (!registered || registered.type !== PluginType.PLUGIN) return undefined |
63 | 66 | ||
@@ -65,7 +68,8 @@ export class PluginManager { | |||
65 | } | 68 | } |
66 | 69 | ||
67 | getRegisteredTheme (name: string) { | 70 | getRegisteredTheme (name: string) { |
68 | const registered = this.getRegisteredPluginOrTheme(name) | 71 | const npmName = PluginModel.buildNpmName(name, PluginType.THEME) |
72 | const registered = this.getRegisteredPluginOrTheme(npmName) | ||
69 | 73 | ||
70 | if (!registered || registered.type !== PluginType.THEME) return undefined | 74 | if (!registered || registered.type !== PluginType.THEME) return undefined |
71 | 75 | ||
@@ -80,8 +84,8 @@ export class PluginManager { | |||
80 | return this.getRegisteredPluginsOrThemes(PluginType.THEME) | 84 | return this.getRegisteredPluginsOrThemes(PluginType.THEME) |
81 | } | 85 | } |
82 | 86 | ||
83 | getSettings (name: string) { | 87 | getRegisteredSettings (npmName: string) { |
84 | return this.settings[name] || [] | 88 | return this.settings[npmName] || [] |
85 | } | 89 | } |
86 | 90 | ||
87 | // ###################### Hooks ###################### | 91 | // ###################### Hooks ###################### |
@@ -126,35 +130,36 @@ export class PluginManager { | |||
126 | this.sortHooksByPriority() | 130 | this.sortHooksByPriority() |
127 | } | 131 | } |
128 | 132 | ||
129 | async unregister (name: string) { | 133 | // Don't need the plugin type since themes cannot register server code |
130 | const plugin = this.getRegisteredPlugin(name) | 134 | async unregister (npmName: string) { |
135 | logger.info('Unregister plugin %s.', npmName) | ||
136 | |||
137 | const plugin = this.getRegisteredPluginOrTheme(npmName) | ||
131 | 138 | ||
132 | if (!plugin) { | 139 | if (!plugin) { |
133 | throw new Error(`Unknown plugin ${name} to unregister`) | 140 | throw new Error(`Unknown plugin ${npmName} to unregister`) |
134 | } | 141 | } |
135 | 142 | ||
136 | if (plugin.type === PluginType.THEME) { | 143 | if (plugin.type === PluginType.PLUGIN) { |
137 | throw new Error(`Cannot unregister ${name}: this is a theme`) | 144 | await plugin.unregister() |
138 | } | ||
139 | 145 | ||
140 | await plugin.unregister() | 146 | // Remove hooks of this plugin |
147 | for (const key of Object.keys(this.hooks)) { | ||
148 | this.hooks[key] = this.hooks[key].filter(h => h.pluginName !== npmName) | ||
149 | } | ||
141 | 150 | ||
142 | // Remove hooks of this plugin | 151 | logger.info('Regenerating registered plugin CSS to global file.') |
143 | for (const key of Object.keys(this.hooks)) { | 152 | await this.regeneratePluginGlobalCSS() |
144 | this.hooks[key] = this.hooks[key].filter(h => h.pluginName !== name) | ||
145 | } | 153 | } |
146 | 154 | ||
147 | delete this.registeredPlugins[plugin.name] | 155 | delete this.registeredPlugins[plugin.npmName] |
148 | |||
149 | logger.info('Regenerating registered plugin CSS to global file.') | ||
150 | await this.regeneratePluginGlobalCSS() | ||
151 | } | 156 | } |
152 | 157 | ||
153 | // ###################### Installation ###################### | 158 | // ###################### Installation ###################### |
154 | 159 | ||
155 | async install (toInstall: string, version?: string, fromDisk = false) { | 160 | async install (toInstall: string, version?: string, fromDisk = false) { |
156 | let plugin: PluginModel | 161 | let plugin: PluginModel |
157 | let name: string | 162 | let npmName: string |
158 | 163 | ||
159 | logger.info('Installing plugin %s.', toInstall) | 164 | logger.info('Installing plugin %s.', toInstall) |
160 | 165 | ||
@@ -163,9 +168,9 @@ export class PluginManager { | |||
163 | ? await installNpmPluginFromDisk(toInstall) | 168 | ? await installNpmPluginFromDisk(toInstall) |
164 | : await installNpmPlugin(toInstall, version) | 169 | : await installNpmPlugin(toInstall, version) |
165 | 170 | ||
166 | name = fromDisk ? basename(toInstall) : toInstall | 171 | npmName = fromDisk ? basename(toInstall) : toInstall |
167 | const pluginType = PluginModel.getTypeFromNpmName(name) | 172 | const pluginType = PluginModel.getTypeFromNpmName(npmName) |
168 | const pluginName = PluginModel.normalizePluginName(name) | 173 | const pluginName = PluginModel.normalizePluginName(npmName) |
169 | 174 | ||
170 | const packageJSON = this.getPackageJSON(pluginName, pluginType) | 175 | const packageJSON = this.getPackageJSON(pluginName, pluginType) |
171 | if (!isPackageJSONValid(packageJSON, pluginType)) { | 176 | if (!isPackageJSONValid(packageJSON, pluginType)) { |
@@ -186,7 +191,7 @@ export class PluginManager { | |||
186 | logger.error('Cannot install plugin %s, removing it...', toInstall, { err }) | 191 | logger.error('Cannot install plugin %s, removing it...', toInstall, { err }) |
187 | 192 | ||
188 | try { | 193 | try { |
189 | await removeNpmPlugin(name) | 194 | await removeNpmPlugin(npmName) |
190 | } catch (err) { | 195 | } catch (err) { |
191 | logger.error('Cannot remove plugin %s after failed installation.', toInstall, { err }) | 196 | logger.error('Cannot remove plugin %s after failed installation.', toInstall, { err }) |
192 | } | 197 | } |
@@ -197,17 +202,28 @@ export class PluginManager { | |||
197 | logger.info('Successful installation of plugin %s.', toInstall) | 202 | logger.info('Successful installation of plugin %s.', toInstall) |
198 | 203 | ||
199 | await this.registerPluginOrTheme(plugin) | 204 | await this.registerPluginOrTheme(plugin) |
205 | |||
206 | return plugin | ||
207 | } | ||
208 | |||
209 | async update (toUpdate: string, version?: string, fromDisk = false) { | ||
210 | const npmName = fromDisk ? basename(toUpdate) : toUpdate | ||
211 | |||
212 | logger.info('Updating plugin %s.', npmName) | ||
213 | |||
214 | // Unregister old hooks | ||
215 | await this.unregister(npmName) | ||
216 | |||
217 | return this.install(toUpdate, version, fromDisk) | ||
200 | } | 218 | } |
201 | 219 | ||
202 | async uninstall (npmName: string) { | 220 | async uninstall (npmName: string) { |
203 | logger.info('Uninstalling plugin %s.', npmName) | 221 | logger.info('Uninstalling plugin %s.', npmName) |
204 | 222 | ||
205 | const pluginName = PluginModel.normalizePluginName(npmName) | ||
206 | |||
207 | try { | 223 | try { |
208 | await this.unregister(pluginName) | 224 | await this.unregister(npmName) |
209 | } catch (err) { | 225 | } catch (err) { |
210 | logger.warn('Cannot unregister plugin %s.', pluginName, { err }) | 226 | logger.warn('Cannot unregister plugin %s.', npmName, { err }) |
211 | } | 227 | } |
212 | 228 | ||
213 | const plugin = await PluginModel.loadByNpmName(npmName) | 229 | const plugin = await PluginModel.loadByNpmName(npmName) |
@@ -229,7 +245,9 @@ export class PluginManager { | |||
229 | // ###################### Private register ###################### | 245 | // ###################### Private register ###################### |
230 | 246 | ||
231 | private async registerPluginOrTheme (plugin: PluginModel) { | 247 | private async registerPluginOrTheme (plugin: PluginModel) { |
232 | logger.info('Registering plugin or theme %s.', plugin.name) | 248 | const npmName = PluginModel.buildNpmName(plugin.name, plugin.type) |
249 | |||
250 | logger.info('Registering plugin or theme %s.', npmName) | ||
233 | 251 | ||
234 | const packageJSON = this.getPackageJSON(plugin.name, plugin.type) | 252 | const packageJSON = this.getPackageJSON(plugin.name, plugin.type) |
235 | const pluginPath = this.getPluginPath(plugin.name, plugin.type) | 253 | const pluginPath = this.getPluginPath(plugin.name, plugin.type) |
@@ -248,7 +266,8 @@ export class PluginManager { | |||
248 | clientScripts[c.script] = c | 266 | clientScripts[c.script] = c |
249 | } | 267 | } |
250 | 268 | ||
251 | this.registeredPlugins[ plugin.name ] = { | 269 | this.registeredPlugins[ npmName ] = { |
270 | npmName, | ||
252 | name: plugin.name, | 271 | name: plugin.name, |
253 | type: plugin.type, | 272 | type: plugin.type, |
254 | version: plugin.version, | 273 | version: plugin.version, |
@@ -263,10 +282,13 @@ export class PluginManager { | |||
263 | } | 282 | } |
264 | 283 | ||
265 | private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) { | 284 | private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) { |
285 | const npmName = PluginModel.buildNpmName(plugin.name, plugin.type) | ||
286 | |||
266 | const registerHook = (options: RegisterHookOptions) => { | 287 | const registerHook = (options: RegisterHookOptions) => { |
267 | if (!this.hooks[options.target]) this.hooks[options.target] = [] | 288 | if (!this.hooks[options.target]) this.hooks[options.target] = [] |
268 | 289 | ||
269 | this.hooks[options.target].push({ | 290 | this.hooks[options.target].push({ |
291 | npmName, | ||
270 | pluginName: plugin.name, | 292 | pluginName: plugin.name, |
271 | handler: options.handler, | 293 | handler: options.handler, |
272 | priority: options.priority || 0 | 294 | priority: options.priority || 0 |
@@ -274,15 +296,15 @@ export class PluginManager { | |||
274 | } | 296 | } |
275 | 297 | ||
276 | const registerSetting = (options: RegisterSettingOptions) => { | 298 | const registerSetting = (options: RegisterSettingOptions) => { |
277 | if (!this.settings[plugin.name]) this.settings[plugin.name] = [] | 299 | if (!this.settings[npmName]) this.settings[npmName] = [] |
278 | 300 | ||
279 | this.settings[plugin.name].push(options) | 301 | this.settings[npmName].push(options) |
280 | } | 302 | } |
281 | 303 | ||
282 | const settingsManager: PluginSettingsManager = { | 304 | const settingsManager: PluginSettingsManager = { |
283 | getSetting: (name: string) => PluginModel.getSetting(plugin.name, name), | 305 | getSetting: (name: string) => PluginModel.getSetting(plugin.name, plugin.type, name), |
284 | 306 | ||
285 | setSetting: (name: string, value: string) => PluginModel.setSetting(plugin.name, name, value) | 307 | setSetting: (name: string, value: string) => PluginModel.setSetting(plugin.name, plugin.type, name, value) |
286 | } | 308 | } |
287 | 309 | ||
288 | const library: PluginLibrary = require(join(pluginPath, packageJSON.library)) | 310 | const library: PluginLibrary = require(join(pluginPath, packageJSON.library)) |
@@ -293,7 +315,7 @@ export class PluginManager { | |||
293 | 315 | ||
294 | library.register({ registerHook, registerSetting, settingsManager }) | 316 | library.register({ registerHook, registerSetting, settingsManager }) |
295 | 317 | ||
296 | logger.info('Add plugin %s CSS to global file.', plugin.name) | 318 | logger.info('Add plugin %s CSS to global file.', npmName) |
297 | 319 | ||
298 | await this.addCSSToGlobalFile(pluginPath, packageJSON.css) | 320 | await this.addCSSToGlobalFile(pluginPath, packageJSON.css) |
299 | 321 | ||
@@ -351,9 +373,9 @@ export class PluginManager { | |||
351 | } | 373 | } |
352 | 374 | ||
353 | private getPluginPath (pluginName: string, pluginType: PluginType) { | 375 | private getPluginPath (pluginName: string, pluginType: PluginType) { |
354 | const prefix = pluginType === PluginType.PLUGIN ? 'peertube-plugin-' : 'peertube-theme-' | 376 | const npmName = PluginModel.buildNpmName(pluginName, pluginType) |
355 | 377 | ||
356 | return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', prefix + pluginName) | 378 | return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName) |
357 | } | 379 | } |
358 | 380 | ||
359 | // ###################### Private getters ###################### | 381 | // ###################### Private getters ###################### |
@@ -361,8 +383,8 @@ export class PluginManager { | |||
361 | private getRegisteredPluginsOrThemes (type: PluginType) { | 383 | private getRegisteredPluginsOrThemes (type: PluginType) { |
362 | const plugins: RegisteredPlugin[] = [] | 384 | const plugins: RegisteredPlugin[] = [] |
363 | 385 | ||
364 | for (const pluginName of Object.keys(this.registeredPlugins)) { | 386 | for (const npmName of Object.keys(this.registeredPlugins)) { |
365 | const plugin = this.registeredPlugins[ pluginName ] | 387 | const plugin = this.registeredPlugins[ npmName ] |
366 | if (plugin.type !== type) continue | 388 | if (plugin.type !== type) continue |
367 | 389 | ||
368 | plugins.push(plugin) | 390 | plugins.push(plugin) |
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts index a1634ded4..8103ec7d3 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts | |||
@@ -1,14 +1,15 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { param, query, body } from 'express-validator/check' | 2 | import { body, param, query } from 'express-validator/check' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import { areValidationErrors } from './utils' | 4 | import { areValidationErrors } from './utils' |
5 | import { isPluginNameValid, isPluginTypeValid, isPluginVersionValid, isNpmPluginNameValid } from '../../helpers/custom-validators/plugins' | 5 | import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' |
6 | import { PluginManager } from '../../lib/plugins/plugin-manager' | 6 | import { PluginManager } from '../../lib/plugins/plugin-manager' |
7 | import { isBooleanValid, isSafePath } from '../../helpers/custom-validators/misc' | 7 | import { isBooleanValid, isSafePath } from '../../helpers/custom-validators/misc' |
8 | import { PluginModel } from '../../models/server/plugin' | 8 | import { PluginModel } from '../../models/server/plugin' |
9 | import { InstallPlugin } from '../../../shared/models/plugins/install-plugin.model' | 9 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' |
10 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | ||
10 | 11 | ||
11 | const servePluginStaticDirectoryValidator = [ | 12 | const servePluginStaticDirectoryValidator = (pluginType: PluginType) => [ |
12 | param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'), | 13 | param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'), |
13 | param('pluginVersion').custom(isPluginVersionValid).withMessage('Should have a valid plugin version'), | 14 | param('pluginVersion').custom(isPluginVersionValid).withMessage('Should have a valid plugin version'), |
14 | param('staticEndpoint').custom(isSafePath).withMessage('Should have a valid static endpoint'), | 15 | param('staticEndpoint').custom(isSafePath).withMessage('Should have a valid static endpoint'), |
@@ -18,7 +19,8 @@ const servePluginStaticDirectoryValidator = [ | |||
18 | 19 | ||
19 | if (areValidationErrors(req, res)) return | 20 | if (areValidationErrors(req, res)) return |
20 | 21 | ||
21 | const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(req.params.pluginName) | 22 | const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType) |
23 | const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName) | ||
22 | 24 | ||
23 | if (!plugin || plugin.version !== req.params.pluginVersion) { | 25 | if (!plugin || plugin.version !== req.params.pluginVersion) { |
24 | return res.sendStatus(404) | 26 | return res.sendStatus(404) |
@@ -48,7 +50,7 @@ const listPluginsValidator = [ | |||
48 | } | 50 | } |
49 | ] | 51 | ] |
50 | 52 | ||
51 | const installPluginValidator = [ | 53 | const installOrUpdatePluginValidator = [ |
52 | body('npmName') | 54 | body('npmName') |
53 | .optional() | 55 | .optional() |
54 | .custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'), | 56 | .custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'), |
@@ -57,11 +59,11 @@ const installPluginValidator = [ | |||
57 | .custom(isSafePath).withMessage('Should have a valid safe path'), | 59 | .custom(isSafePath).withMessage('Should have a valid safe path'), |
58 | 60 | ||
59 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 61 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
60 | logger.debug('Checking installPluginValidator parameters', { parameters: req.body }) | 62 | logger.debug('Checking installOrUpdatePluginValidator parameters', { parameters: req.body }) |
61 | 63 | ||
62 | if (areValidationErrors(req, res)) return | 64 | if (areValidationErrors(req, res)) return |
63 | 65 | ||
64 | const body: InstallPlugin = req.body | 66 | const body: InstallOrUpdatePlugin = req.body |
65 | if (!body.path && !body.npmName) { | 67 | if (!body.path && !body.npmName) { |
66 | return res.status(400) | 68 | return res.status(400) |
67 | .json({ error: 'Should have either a npmName or a path' }) | 69 | .json({ error: 'Should have either a npmName or a path' }) |
@@ -124,6 +126,6 @@ export { | |||
124 | updatePluginSettingsValidator, | 126 | updatePluginSettingsValidator, |
125 | uninstallPluginValidator, | 127 | uninstallPluginValidator, |
126 | existingPluginValidator, | 128 | existingPluginValidator, |
127 | installPluginValidator, | 129 | installOrUpdatePluginValidator, |
128 | listPluginsValidator | 130 | listPluginsValidator |
129 | } | 131 | } |
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index 226c08342..340d49f3b 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { getSort, throwIfNotValid } from '../utils' | 2 | import { getSort, throwIfNotValid } from '../utils' |
3 | import { | 3 | import { |
4 | isPluginDescriptionValid, isPluginHomepage, | 4 | isPluginDescriptionValid, |
5 | isPluginHomepage, | ||
5 | isPluginNameValid, | 6 | isPluginNameValid, |
6 | isPluginTypeValid, | 7 | isPluginTypeValid, |
7 | isPluginVersionValid | 8 | isPluginVersionValid |
@@ -42,6 +43,11 @@ export class PluginModel extends Model<PluginModel> { | |||
42 | @Column | 43 | @Column |
43 | version: string | 44 | version: string |
44 | 45 | ||
46 | @AllowNull(true) | ||
47 | @Is('PluginLatestVersion', value => throwIfNotValid(value, isPluginVersionValid, 'version')) | ||
48 | @Column | ||
49 | latestVersion: string | ||
50 | |||
45 | @AllowNull(false) | 51 | @AllowNull(false) |
46 | @Column | 52 | @Column |
47 | enabled: boolean | 53 | enabled: boolean |
@@ -103,27 +109,28 @@ export class PluginModel extends Model<PluginModel> { | |||
103 | return PluginModel.findOne(query) | 109 | return PluginModel.findOne(query) |
104 | } | 110 | } |
105 | 111 | ||
106 | static getSetting (pluginName: string, settingName: string) { | 112 | static getSetting (pluginName: string, pluginType: PluginType, settingName: string) { |
107 | const query = { | 113 | const query = { |
108 | attributes: [ 'settings' ], | 114 | attributes: [ 'settings' ], |
109 | where: { | 115 | where: { |
110 | name: pluginName | 116 | name: pluginName, |
117 | type: pluginType | ||
111 | } | 118 | } |
112 | } | 119 | } |
113 | 120 | ||
114 | return PluginModel.findOne(query) | 121 | return PluginModel.findOne(query) |
115 | .then(p => p.settings) | 122 | .then(p => { |
116 | .then(settings => { | 123 | if (!p || !p.settings) return undefined |
117 | if (!settings) return undefined | ||
118 | 124 | ||
119 | return settings[settingName] | 125 | return p.settings[settingName] |
120 | }) | 126 | }) |
121 | } | 127 | } |
122 | 128 | ||
123 | static setSetting (pluginName: string, settingName: string, settingValue: string) { | 129 | static setSetting (pluginName: string, pluginType: PluginType, settingName: string, settingValue: string) { |
124 | const query = { | 130 | const query = { |
125 | where: { | 131 | where: { |
126 | name: pluginName | 132 | name: pluginName, |
133 | type: pluginType | ||
127 | } | 134 | } |
128 | } | 135 | } |
129 | 136 | ||
@@ -171,11 +178,18 @@ export class PluginModel extends Model<PluginModel> { | |||
171 | : PluginType.THEME | 178 | : PluginType.THEME |
172 | } | 179 | } |
173 | 180 | ||
181 | static buildNpmName (name: string, type: PluginType) { | ||
182 | if (type === PluginType.THEME) return 'peertube-theme-' + name | ||
183 | |||
184 | return 'peertube-plugin-' + name | ||
185 | } | ||
186 | |||
174 | toFormattedJSON (): PeerTubePlugin { | 187 | toFormattedJSON (): PeerTubePlugin { |
175 | return { | 188 | return { |
176 | name: this.name, | 189 | name: this.name, |
177 | type: this.type, | 190 | type: this.type, |
178 | version: this.version, | 191 | version: this.version, |
192 | latestVersion: this.latestVersion, | ||
179 | enabled: this.enabled, | 193 | enabled: this.enabled, |
180 | uninstalled: this.uninstalled, | 194 | uninstalled: this.uninstalled, |
181 | peertubeEngine: this.peertubeEngine, | 195 | peertubeEngine: this.peertubeEngine, |
diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts index d5e024383..10cff7dd7 100644 --- a/server/tools/peertube-plugins.ts +++ b/server/tools/peertube-plugins.ts | |||
@@ -2,7 +2,7 @@ import * as program from 'commander' | |||
2 | import { PluginType } from '../../shared/models/plugins/plugin.type' | 2 | import { PluginType } from '../../shared/models/plugins/plugin.type' |
3 | import { getAccessToken } from '../../shared/extra-utils/users/login' | 3 | import { getAccessToken } from '../../shared/extra-utils/users/login' |
4 | import { getMyUserInformation } from '../../shared/extra-utils/users/users' | 4 | import { getMyUserInformation } from '../../shared/extra-utils/users/users' |
5 | import { installPlugin, listPlugins, uninstallPlugin } from '../../shared/extra-utils/server/plugins' | 5 | import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins' |
6 | import { getServerCredentials } from './cli' | 6 | import { getServerCredentials } from './cli' |
7 | import { User, UserRole } from '../../shared/models/users' | 7 | import { User, UserRole } from '../../shared/models/users' |
8 | import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model' | 8 | import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model' |
@@ -35,6 +35,16 @@ program | |||
35 | .action((options) => installPluginCLI(options)) | 35 | .action((options) => installPluginCLI(options)) |
36 | 36 | ||
37 | program | 37 | program |
38 | .command('update') | ||
39 | .description('Update a plugin or a theme') | ||
40 | .option('-u, --url <url>', 'Server url') | ||
41 | .option('-U, --username <username>', 'Username') | ||
42 | .option('-p, --password <token>', 'Password') | ||
43 | .option('-P --path <path>', 'Update from a path') | ||
44 | .option('-n, --npm-name <npmName>', 'Update from npm') | ||
45 | .action((options) => updatePluginCLI(options)) | ||
46 | |||
47 | program | ||
38 | .command('uninstall') | 48 | .command('uninstall') |
39 | .description('Uninstall a plugin or a theme') | 49 | .description('Uninstall a plugin or a theme') |
40 | .option('-u, --url <url>', 'Server url') | 50 | .option('-u, --url <url>', 'Server url') |
@@ -122,6 +132,38 @@ async function installPluginCLI (options: any) { | |||
122 | process.exit(0) | 132 | process.exit(0) |
123 | } | 133 | } |
124 | 134 | ||
135 | async function updatePluginCLI (options: any) { | ||
136 | if (!options['path'] && !options['npmName']) { | ||
137 | console.error('You need to specify the npm name or the path of the plugin you want to update.\n') | ||
138 | program.outputHelp() | ||
139 | process.exit(-1) | ||
140 | } | ||
141 | |||
142 | if (options['path'] && !isAbsolute(options['path'])) { | ||
143 | console.error('Path should be absolute.') | ||
144 | process.exit(-1) | ||
145 | } | ||
146 | |||
147 | const { url, username, password } = await getServerCredentials(options) | ||
148 | const accessToken = await getAdminTokenOrDie(url, username, password) | ||
149 | |||
150 | try { | ||
151 | await updatePlugin({ | ||
152 | url, | ||
153 | accessToken, | ||
154 | npmName: options['npmName'], | ||
155 | path: options['path'] | ||
156 | }) | ||
157 | } catch (err) { | ||
158 | console.error('Cannot update plugin.', err) | ||
159 | process.exit(-1) | ||
160 | return | ||
161 | } | ||
162 | |||
163 | console.log('Plugin updated.') | ||
164 | process.exit(0) | ||
165 | } | ||
166 | |||
125 | async function uninstallPluginCLI (options: any) { | 167 | async function uninstallPluginCLI (options: any) { |
126 | if (!options['npmName']) { | 168 | if (!options['npmName']) { |
127 | console.error('You need to specify the npm name of the plugin/theme you want to uninstall.\n') | 169 | console.error('You need to specify the npm name of the plugin/theme you want to uninstall.\n') |