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' import { isAbsolute } from 'path' program - .option('-n, --pluginName [pluginName]', 'Plugin name to install') - .option('-v, --pluginVersion [pluginVersion]', 'Plugin version to install') - .option('-p, --pluginPath [pluginPath]', 'Path of the plugin you want to install') + .option('-n, --plugin-name [pluginName]', 'Plugin name to install') + .option('-v, --plugin-version [pluginVersion]', 'Plugin version to install') + .option('-p, --plugin-path [pluginPath]', 'Path of the plugin you want to install') .parse(process.argv) if (!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 @@ +import { initDatabaseModels } from '../../server/initializers/database' +import * as program from 'commander' +import { PluginManager } from '../../server/lib/plugins/plugin-manager' +import { isAbsolute } from 'path' + +program + .option('-n, --package-name [packageName]', 'Package name to install') + .parse(process.argv) + +if (!program['packageName']) { + console.error('You need to specify the plugin name.') + process.exit(-1) +} + +run() + .then(() => process.exit(0)) + .catch(err => { + console.error(err) + process.exit(-1) + }) + +async function run () { + await initDatabaseModels(true) + + const toUninstall = program['packageName'] + await PluginManager.Instance.uninstall(toUninstall) +} 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 @@ import * as express from 'express' import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' -import { join } from 'path' +import { basename, join } from 'path' import { RegisteredPlugin } from '../lib/plugins/plugin-manager' import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins' const pluginsRouter = express.Router() pluginsRouter.get('/global.css', - express.static(PLUGIN_GLOBAL_CSS_PATH, { fallthrough: false }) + servePluginGlobalCSS ) -pluginsRouter.get('/:pluginName/:pluginVersion/statics/:staticEndpoint', +pluginsRouter.get('/:pluginName/:pluginVersion/static/:staticEndpoint(*)', servePluginStaticDirectoryValidator, servePluginStaticDirectory ) -pluginsRouter.get('/:pluginName/:pluginVersion/client-scripts/:staticEndpoint', +pluginsRouter.get('/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)', servePluginStaticDirectoryValidator, servePluginClientScripts ) @@ -28,21 +28,33 @@ export { // --------------------------------------------------------------------------- +function servePluginGlobalCSS (req: express.Request, res: express.Response) { + return res.sendFile(PLUGIN_GLOBAL_CSS_PATH) +} + function servePluginStaticDirectory (req: express.Request, res: express.Response) { const plugin: RegisteredPlugin = res.locals.registeredPlugin const staticEndpoint = req.params.staticEndpoint - const staticPath = plugin.staticDirs[staticEndpoint] + const [ directory, ...file ] = staticEndpoint.split('/') + + const staticPath = plugin.staticDirs[directory] if (!staticPath) { return res.sendStatus(404) } - return express.static(join(plugin.path, staticPath), { fallthrough: false }) + const filepath = file.join('/') + return res.sendFile(join(plugin.path, staticPath, filepath)) } function servePluginClientScripts (req: express.Request, res: express.Response) { const plugin: RegisteredPlugin = res.locals.registeredPlugin const staticEndpoint = req.params.staticEndpoint - return express.static(join(plugin.path, staticEndpoint), { fallthrough: false }) + const file = plugin.clientScripts[staticEndpoint] + if (!file) { + return res.sendStatus(404) + } + + return res.sendFile(join(plugin.path, staticEndpoint)) } 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 import { basename, join } from 'path' import { CONFIG } from '../../initializers/config' import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' -import { PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' +import { ClientScript, PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' import { PluginLibrary } from '../../../shared/models/plugins/plugin-library.model' import { createReadStream, createWriteStream } from 'fs' import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants' import { PluginType } from '../../../shared/models/plugins/plugin.type' import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' +import { outputFile } from 'fs-extra' export interface RegisteredPlugin { name: string @@ -22,6 +23,7 @@ export interface RegisteredPlugin { path: string staticDirs: { [name: string]: string } + clientScripts: { [name: string]: ClientScript } css: string[] @@ -46,6 +48,8 @@ export class PluginManager { } async registerPlugins () { + await this.resetCSSGlobalFile() + const plugins = await PluginModel.listEnabledPluginsAndThemes() for (const plugin of plugins) { @@ -83,6 +87,16 @@ export class PluginManager { } await plugin.unregister() + + // Remove hooks of this plugin + for (const key of Object.keys(this.hooks)) { + this.hooks[key] = this.hooks[key].filter(h => h.pluginName !== name) + } + + delete this.registeredPlugins[plugin.name] + + logger.info('Regenerating registered plugin CSS to global file.') + await this.regeneratePluginGlobalCSS() } async install (toInstall: string, version: string, fromDisk = false) { @@ -132,9 +146,30 @@ export class PluginManager { } async uninstall (packageName: string) { - await PluginModel.uninstall(this.normalizePluginName(packageName)) + logger.info('Uninstalling plugin %s.', packageName) + + const pluginName = this.normalizePluginName(packageName) + + try { + await this.unregister(pluginName) + } catch (err) { + logger.warn('Cannot unregister plugin %s.', pluginName, { err }) + } + + const plugin = await PluginModel.load(pluginName) + if (!plugin || plugin.uninstalled === true) { + logger.error('Cannot uninstall plugin %s: it does not exist or is already uninstalled.', packageName) + return + } + + plugin.enabled = false + plugin.uninstalled = true + + await plugin.save() await removeNpmPlugin(packageName) + + logger.info('Plugin %s uninstalled.', packageName) } private async registerPluginOrTheme (plugin: PluginModel) { @@ -152,6 +187,11 @@ export class PluginManager { library = await this.registerPlugin(plugin, pluginPath, packageJSON) } + const clientScripts: { [id: string]: ClientScript } = {} + for (const c of packageJSON.clientScripts) { + clientScripts[c.script] = c + } + this.registeredPlugins[ plugin.name ] = { name: plugin.name, type: plugin.type, @@ -160,6 +200,7 @@ export class PluginManager { peertubeEngine: plugin.peertubeEngine, path: pluginPath, staticDirs: packageJSON.staticDirs, + clientScripts, css: packageJSON.css, unregister: library ? library.unregister : undefined } @@ -199,6 +240,10 @@ export class PluginManager { } } + private resetCSSGlobalFile () { + return outputFile(PLUGIN_GLOBAL_CSS_PATH, '') + } + private async addCSSToGlobalFile (pluginPath: string, cssRelativePaths: string[]) { for (const cssPath of cssRelativePaths) { await this.concatFiles(join(pluginPath, cssPath), PLUGIN_GLOBAL_CSS_PATH) @@ -207,8 +252,8 @@ export class PluginManager { private concatFiles (input: string, output: string) { return new Promise((res, rej) => { - const outputStream = createWriteStream(input) - const inputStream = createReadStream(output) + const inputStream = createReadStream(input) + const outputStream = createWriteStream(output, { flags: 'a' }) inputStream.pipe(outputStream) @@ -233,6 +278,16 @@ export class PluginManager { return name.replace(/^peertube-((theme)|(plugin))-/, '') } + private async regeneratePluginGlobalCSS () { + await this.resetCSSGlobalFile() + + for (const key of Object.keys(this.registeredPlugins)) { + const plugin = this.registeredPlugins[key] + + await this.addCSSToGlobalFile(plugin.path, plugin.css) + } + } + static get Instance () { return this.instance || (this.instance = new this()) } 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 { return PluginModel.findAll(query) } + static load (pluginName: string) { + const query = { + where: { + name: pluginName + } + } + + return PluginModel.findOne(query) + } + static uninstall (pluginName: string) { const query = { 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 @@ +export type ClientScript = { + script: string, + scopes: string[] +} + export type PluginPackageJson = { name: string version: string @@ -12,5 +17,5 @@ export type PluginPackageJson = { staticDirs: { [ name: string ]: string } css: string[] - clientScripts: { script: string, scopes: string[] }[] + clientScripts: ClientScript[] }