aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rwxr-xr-xscripts/plugin/install.ts6
-rwxr-xr-xscripts/plugin/uninstall.ts27
-rw-r--r--server/controllers/plugins.ts26
-rw-r--r--server/lib/plugins/plugin-manager.ts63
-rw-r--r--server/models/server/plugin.ts10
-rw-r--r--shared/models/plugins/plugin-package-json.model.ts7
6 files changed, 124 insertions, 15 deletions
diff --git a/scripts/plugin/install.ts b/scripts/plugin/install.ts
index 8e9c9897f..1725cbeb6 100755
--- a/scripts/plugin/install.ts
+++ b/scripts/plugin/install.ts
@@ -4,9 +4,9 @@ import { PluginManager } from '../../server/lib/plugins/plugin-manager'
4import { isAbsolute } from 'path' 4import { isAbsolute } from 'path'
5 5
6program 6program
7 .option('-n, --pluginName [pluginName]', 'Plugin name to install') 7 .option('-n, --plugin-name [pluginName]', 'Plugin name to install')
8 .option('-v, --pluginVersion [pluginVersion]', 'Plugin version to install') 8 .option('-v, --plugin-version [pluginVersion]', 'Plugin version to install')
9 .option('-p, --pluginPath [pluginPath]', 'Path of the plugin you want to install') 9 .option('-p, --plugin-path [pluginPath]', 'Path of the plugin you want to install')
10 .parse(process.argv) 10 .parse(process.argv)
11 11
12if (!program['pluginName'] && !program['pluginPath']) { 12if (!program['pluginName'] && !program['pluginPath']) {
diff --git a/scripts/plugin/uninstall.ts b/scripts/plugin/uninstall.ts
new file mode 100755
index 000000000..7dcc234db
--- /dev/null
+++ b/scripts/plugin/uninstall.ts
@@ -0,0 +1,27 @@
1import { initDatabaseModels } from '../../server/initializers/database'
2import * as program from 'commander'
3import { PluginManager } from '../../server/lib/plugins/plugin-manager'
4import { isAbsolute } from 'path'
5
6program
7 .option('-n, --package-name [packageName]', 'Package name to install')
8 .parse(process.argv)
9
10if (!program['packageName']) {
11 console.error('You need to specify the plugin name.')
12 process.exit(-1)
13}
14
15run()
16 .then(() => process.exit(0))
17 .catch(err => {
18 console.error(err)
19 process.exit(-1)
20 })
21
22async function run () {
23 await initDatabaseModels(true)
24
25 const toUninstall = program['packageName']
26 await PluginManager.Instance.uninstall(toUninstall)
27}
diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts
index a6705d9c7..05f03324d 100644
--- a/server/controllers/plugins.ts
+++ b/server/controllers/plugins.ts
@@ -1,21 +1,21 @@
1import * as express from 'express' 1import * as express from 'express'
2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' 2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
3import { join } from 'path' 3import { basename, join } from 'path'
4import { RegisteredPlugin } from '../lib/plugins/plugin-manager' 4import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
5import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins' 5import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
6 6
7const pluginsRouter = express.Router() 7const pluginsRouter = express.Router()
8 8
9pluginsRouter.get('/global.css', 9pluginsRouter.get('/global.css',
10 express.static(PLUGIN_GLOBAL_CSS_PATH, { fallthrough: false }) 10 servePluginGlobalCSS
11) 11)
12 12
13pluginsRouter.get('/:pluginName/:pluginVersion/statics/:staticEndpoint', 13pluginsRouter.get('/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
14 servePluginStaticDirectoryValidator, 14 servePluginStaticDirectoryValidator,
15 servePluginStaticDirectory 15 servePluginStaticDirectory
16) 16)
17 17
18pluginsRouter.get('/:pluginName/:pluginVersion/client-scripts/:staticEndpoint', 18pluginsRouter.get('/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)',
19 servePluginStaticDirectoryValidator, 19 servePluginStaticDirectoryValidator,
20 servePluginClientScripts 20 servePluginClientScripts
21) 21)
@@ -28,21 +28,33 @@ export {
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
31function servePluginGlobalCSS (req: express.Request, res: express.Response) {
32 return res.sendFile(PLUGIN_GLOBAL_CSS_PATH)
33}
34
31function servePluginStaticDirectory (req: express.Request, res: express.Response) { 35function servePluginStaticDirectory (req: express.Request, res: express.Response) {
32 const plugin: RegisteredPlugin = res.locals.registeredPlugin 36 const plugin: RegisteredPlugin = res.locals.registeredPlugin
33 const staticEndpoint = req.params.staticEndpoint 37 const staticEndpoint = req.params.staticEndpoint
34 38
35 const staticPath = plugin.staticDirs[staticEndpoint] 39 const [ directory, ...file ] = staticEndpoint.split('/')
40
41 const staticPath = plugin.staticDirs[directory]
36 if (!staticPath) { 42 if (!staticPath) {
37 return res.sendStatus(404) 43 return res.sendStatus(404)
38 } 44 }
39 45
40 return express.static(join(plugin.path, staticPath), { fallthrough: false }) 46 const filepath = file.join('/')
47 return res.sendFile(join(plugin.path, staticPath, filepath))
41} 48}
42 49
43function servePluginClientScripts (req: express.Request, res: express.Response) { 50function servePluginClientScripts (req: express.Request, res: express.Response) {
44 const plugin: RegisteredPlugin = res.locals.registeredPlugin 51 const plugin: RegisteredPlugin = res.locals.registeredPlugin
45 const staticEndpoint = req.params.staticEndpoint 52 const staticEndpoint = req.params.staticEndpoint
46 53
47 return express.static(join(plugin.path, staticEndpoint), { fallthrough: false }) 54 const file = plugin.clientScripts[staticEndpoint]
55 if (!file) {
56 return res.sendStatus(404)
57 }
58
59 return res.sendFile(join(plugin.path, staticEndpoint))
48} 60}
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 533ed4391..b898e64fa 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -4,12 +4,13 @@ import { RegisterHookOptions } from '../../../shared/models/plugins/register.mod
4import { basename, join } from 'path' 4import { basename, join } from 'path'
5import { CONFIG } from '../../initializers/config' 5import { CONFIG } from '../../initializers/config'
6import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' 6import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins'
7import { PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' 7import { ClientScript, PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model'
8import { PluginLibrary } from '../../../shared/models/plugins/plugin-library.model' 8import { PluginLibrary } from '../../../shared/models/plugins/plugin-library.model'
9import { createReadStream, createWriteStream } from 'fs' 9import { createReadStream, createWriteStream } from 'fs'
10import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants' 10import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
11import { PluginType } from '../../../shared/models/plugins/plugin.type' 11import { PluginType } from '../../../shared/models/plugins/plugin.type'
12import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' 12import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
13import { outputFile } from 'fs-extra'
13 14
14export interface RegisteredPlugin { 15export interface RegisteredPlugin {
15 name: string 16 name: string
@@ -22,6 +23,7 @@ export interface RegisteredPlugin {
22 path: string 23 path: string
23 24
24 staticDirs: { [name: string]: string } 25 staticDirs: { [name: string]: string }
26 clientScripts: { [name: string]: ClientScript }
25 27
26 css: string[] 28 css: string[]
27 29
@@ -46,6 +48,8 @@ export class PluginManager {
46 } 48 }
47 49
48 async registerPlugins () { 50 async registerPlugins () {
51 await this.resetCSSGlobalFile()
52
49 const plugins = await PluginModel.listEnabledPluginsAndThemes() 53 const plugins = await PluginModel.listEnabledPluginsAndThemes()
50 54
51 for (const plugin of plugins) { 55 for (const plugin of plugins) {
@@ -83,6 +87,16 @@ export class PluginManager {
83 } 87 }
84 88
85 await plugin.unregister() 89 await plugin.unregister()
90
91 // Remove hooks of this plugin
92 for (const key of Object.keys(this.hooks)) {
93 this.hooks[key] = this.hooks[key].filter(h => h.pluginName !== name)
94 }
95
96 delete this.registeredPlugins[plugin.name]
97
98 logger.info('Regenerating registered plugin CSS to global file.')
99 await this.regeneratePluginGlobalCSS()
86 } 100 }
87 101
88 async install (toInstall: string, version: string, fromDisk = false) { 102 async install (toInstall: string, version: string, fromDisk = false) {
@@ -132,9 +146,30 @@ export class PluginManager {
132 } 146 }
133 147
134 async uninstall (packageName: string) { 148 async uninstall (packageName: string) {
135 await PluginModel.uninstall(this.normalizePluginName(packageName)) 149 logger.info('Uninstalling plugin %s.', packageName)
150
151 const pluginName = this.normalizePluginName(packageName)
152
153 try {
154 await this.unregister(pluginName)
155 } catch (err) {
156 logger.warn('Cannot unregister plugin %s.', pluginName, { err })
157 }
158
159 const plugin = await PluginModel.load(pluginName)
160 if (!plugin || plugin.uninstalled === true) {
161 logger.error('Cannot uninstall plugin %s: it does not exist or is already uninstalled.', packageName)
162 return
163 }
164
165 plugin.enabled = false
166 plugin.uninstalled = true
167
168 await plugin.save()
136 169
137 await removeNpmPlugin(packageName) 170 await removeNpmPlugin(packageName)
171
172 logger.info('Plugin %s uninstalled.', packageName)
138 } 173 }
139 174
140 private async registerPluginOrTheme (plugin: PluginModel) { 175 private async registerPluginOrTheme (plugin: PluginModel) {
@@ -152,6 +187,11 @@ export class PluginManager {
152 library = await this.registerPlugin(plugin, pluginPath, packageJSON) 187 library = await this.registerPlugin(plugin, pluginPath, packageJSON)
153 } 188 }
154 189
190 const clientScripts: { [id: string]: ClientScript } = {}
191 for (const c of packageJSON.clientScripts) {
192 clientScripts[c.script] = c
193 }
194
155 this.registeredPlugins[ plugin.name ] = { 195 this.registeredPlugins[ plugin.name ] = {
156 name: plugin.name, 196 name: plugin.name,
157 type: plugin.type, 197 type: plugin.type,
@@ -160,6 +200,7 @@ export class PluginManager {
160 peertubeEngine: plugin.peertubeEngine, 200 peertubeEngine: plugin.peertubeEngine,
161 path: pluginPath, 201 path: pluginPath,
162 staticDirs: packageJSON.staticDirs, 202 staticDirs: packageJSON.staticDirs,
203 clientScripts,
163 css: packageJSON.css, 204 css: packageJSON.css,
164 unregister: library ? library.unregister : undefined 205 unregister: library ? library.unregister : undefined
165 } 206 }
@@ -199,6 +240,10 @@ export class PluginManager {
199 } 240 }
200 } 241 }
201 242
243 private resetCSSGlobalFile () {
244 return outputFile(PLUGIN_GLOBAL_CSS_PATH, '')
245 }
246
202 private async addCSSToGlobalFile (pluginPath: string, cssRelativePaths: string[]) { 247 private async addCSSToGlobalFile (pluginPath: string, cssRelativePaths: string[]) {
203 for (const cssPath of cssRelativePaths) { 248 for (const cssPath of cssRelativePaths) {
204 await this.concatFiles(join(pluginPath, cssPath), PLUGIN_GLOBAL_CSS_PATH) 249 await this.concatFiles(join(pluginPath, cssPath), PLUGIN_GLOBAL_CSS_PATH)
@@ -207,8 +252,8 @@ export class PluginManager {
207 252
208 private concatFiles (input: string, output: string) { 253 private concatFiles (input: string, output: string) {
209 return new Promise<void>((res, rej) => { 254 return new Promise<void>((res, rej) => {
210 const outputStream = createWriteStream(input) 255 const inputStream = createReadStream(input)
211 const inputStream = createReadStream(output) 256 const outputStream = createWriteStream(output, { flags: 'a' })
212 257
213 inputStream.pipe(outputStream) 258 inputStream.pipe(outputStream)
214 259
@@ -233,6 +278,16 @@ export class PluginManager {
233 return name.replace(/^peertube-((theme)|(plugin))-/, '') 278 return name.replace(/^peertube-((theme)|(plugin))-/, '')
234 } 279 }
235 280
281 private async regeneratePluginGlobalCSS () {
282 await this.resetCSSGlobalFile()
283
284 for (const key of Object.keys(this.registeredPlugins)) {
285 const plugin = this.registeredPlugins[key]
286
287 await this.addCSSToGlobalFile(plugin.path, plugin.css)
288 }
289 }
290
236 static get Instance () { 291 static get Instance () {
237 return this.instance || (this.instance = new this()) 292 return this.instance || (this.instance = new this())
238 } 293 }
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts
index 1fbfd208f..b3b8276df 100644
--- a/server/models/server/plugin.ts
+++ b/server/models/server/plugin.ts
@@ -75,6 +75,16 @@ export class PluginModel extends Model<PluginModel> {
75 return PluginModel.findAll(query) 75 return PluginModel.findAll(query)
76 } 76 }
77 77
78 static load (pluginName: string) {
79 const query = {
80 where: {
81 name: pluginName
82 }
83 }
84
85 return PluginModel.findOne(query)
86 }
87
78 static uninstall (pluginName: string) { 88 static uninstall (pluginName: string) {
79 const query = { 89 const query = {
80 where: { 90 where: {
diff --git a/shared/models/plugins/plugin-package-json.model.ts b/shared/models/plugins/plugin-package-json.model.ts
index d5aa90179..f8029ec34 100644
--- a/shared/models/plugins/plugin-package-json.model.ts
+++ b/shared/models/plugins/plugin-package-json.model.ts
@@ -1,3 +1,8 @@
1export type ClientScript = {
2 script: string,
3 scopes: string[]
4}
5
1export type PluginPackageJson = { 6export type PluginPackageJson = {
2 name: string 7 name: string
3 version: string 8 version: string
@@ -12,5 +17,5 @@ export type PluginPackageJson = {
12 staticDirs: { [ name: string ]: string } 17 staticDirs: { [ name: string ]: string }
13 css: string[] 18 css: string[]
14 19
15 clientScripts: { script: string, scopes: string[] }[] 20 clientScripts: ClientScript[]
16} 21}