diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala index ac915b2..0cef067 100644 --- a/src/main/scala/app/SystemSettingsController.scala +++ b/src/main/scala/app/SystemSettingsController.scala @@ -4,10 +4,13 @@ import SystemSettingsService._ import util.AdminAuthenticator import util.Directory._ +import util.ControlUtil._ import jp.sf.amateras.scalatra.forms._ import ssh.SshServer import org.scalatra.Ok import org.apache.commons.io.FileUtils +import java.io.FileInputStream +import plugin.PluginSystem class SystemSettingsController extends SystemSettingsControllerBase with AccountService with AdminAuthenticator @@ -89,11 +92,28 @@ val dir = new java.io.File(PluginHome, pluginId) if(dir.exists && dir.isDirectory){ FileUtils.deleteQuietly(dir) + PluginSystem.uninstall(pluginId) } } redirect("/admin/plugins") }) + get("/admin/plugins/available")(adminOnly { + admin.plugins.html.available(getAvailablePlugins()) + }) + + post("/admin/plugins/_install", pluginForm)(adminOnly { form => + val dir = getPluginCacheDir() + getAvailablePlugins().filter(x => form.pluginIds.contains(x.id)).foreach { plugin => + val pluginDir = new java.io.File(PluginHome, plugin.id) + if(!pluginDir.exists){ + FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir) + } + PluginSystem.installPlugin(plugin.id) + } + redirect("/admin/plugins") + }) + get("/admin/plugins/console")(adminOnly { admin.plugins.html.console() }) @@ -103,4 +123,35 @@ val result = plugin.JavaScriptPlugin.evaluateJavaScript(script) Ok(result) }) + + // TODO Move to PluginSystem or Service? + private def getAvailablePlugins(): List[SystemSettingsControllerBase.AvailablePlugin] = { + val dir = getPluginCacheDir() + if(dir.exists && dir.isDirectory){ + PluginSystem.repositories.flatMap { repo => + val repoDir = new java.io.File(dir, repo.id) + if(repoDir.exists && repoDir.isDirectory){ + repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin => + val propertyFile = new java.io.File(plugin, "plugin.properties") + val properties = new java.util.Properties() + if(propertyFile.exists && propertyFile.isFile){ + using(new FileInputStream(propertyFile)){ in => + properties.load(in) + } + } + SystemSettingsControllerBase.AvailablePlugin( + repo.id, + properties.getProperty("id"), + properties.getProperty("author"), + properties.getProperty("url"), + properties.getProperty("description")) + } + } else Nil + } + } else Nil + } +} + +object SystemSettingsControllerBase { + case class AvailablePlugin(repository: String, id: String, author: String, url: String, description: String) } diff --git a/src/main/scala/plugin/JavaScriptPlugin.scala b/src/main/scala/plugin/JavaScriptPlugin.scala index 7984c32..739e035 100644 --- a/src/main/scala/plugin/JavaScriptPlugin.scala +++ b/src/main/scala/plugin/JavaScriptPlugin.scala @@ -67,12 +67,15 @@ def define(id: String, author: String, url: String, description: String) = new JavaScriptPlugin(id, author, url, description) - def evaluateJavaScript(script: String): Any = { + def evaluateJavaScript(script: String, vars: Map[String, Any] = Map.empty): Any = { val context = JsContext.enter() try { val scope = context.initStandardObjects() scope.put("PluginSystem", scope, PluginSystem) scope.put("JavaScriptPlugin", scope, this) + vars.foreach { case (key, value) => + scope.put(key, scope, value) + } val result = context.evaluateString(scope, script, "", 1, null) result } finally { diff --git a/src/main/scala/plugin/PluginSystem.scala b/src/main/scala/plugin/PluginSystem.scala index 1956ec6..e2477c1 100644 --- a/src/main/scala/plugin/PluginSystem.scala +++ b/src/main/scala/plugin/PluginSystem.scala @@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory import java.util.concurrent.atomic.AtomicBoolean import util.Directory._ +import util.ControlUtil._ import org.apache.commons.io.FileUtils /** @@ -16,6 +17,7 @@ private val initialized = new AtomicBoolean(false) private val pluginsMap = scala.collection.mutable.Map[String, Plugin]() + private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]() def install(plugin: Plugin): Unit = { pluginsMap.put(plugin.id, plugin) @@ -27,25 +29,46 @@ pluginsMap.remove(id) } + def repositories: List[PluginRepository] = repositoriesList.toList + /** * Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins. */ def init(): Unit = { if(initialized.compareAndSet(false, true)){ + // Load installed plugins val pluginDir = new java.io.File(PluginHome) if(pluginDir.exists && pluginDir.isDirectory){ pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir => - val file = new java.io.File(dir, "plugin.js") - if(file.exists && file.isFile){ - val script = FileUtils.readFileToString(file, "UTF-8") - try { - JavaScriptPlugin.evaluateJavaScript(script) - } catch { - case e: Exception => logger.warn(s"Error in plugin loading for ${file.getAbsolutePath}", e) - } - } + installPlugin(dir.getName) } } + // Add default plugin repositories + repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git") + } + } + + def installPlugin(id: String): Unit = { + val pluginDir = new java.io.File(PluginHome) + val javaScriptFile = new java.io.File(pluginDir, id + "/plugin.js") + + if(javaScriptFile.exists && javaScriptFile.isFile){ + val properties = new java.util.Properties() + using(new java.io.FileInputStream(new java.io.File(pluginDir, id + "/plugin.properties"))){ in => + properties.load(in) + } + + val script = FileUtils.readFileToString(javaScriptFile, "UTF-8") + try { + JavaScriptPlugin.evaluateJavaScript(script, Map( + "id" -> properties.getProperty("id"), + "author" -> properties.getProperty("author"), + "url" -> properties.getProperty("url"), + "description" -> properties.getProperty("description") + )) + } catch { + case e: Exception => logger.warn(s"Error in plugin loading for ${javaScriptFile.getAbsolutePath}", e) + } } } @@ -55,6 +78,7 @@ def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList // Case classes to hold plug-ins information internally in GitBucket + case class PluginRepository(id: String, url: String) case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean) case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean) case class Action(path: String, function: (HttpServletRequest, HttpServletResponse) => Any) diff --git a/src/main/scala/util/Directory.scala b/src/main/scala/util/Directory.scala index dac409f..920b22a 100644 --- a/src/main/scala/util/Directory.scala +++ b/src/main/scala/util/Directory.scala @@ -36,6 +36,8 @@ val PluginHome = s"${GitBucketHome}/plugins" + val TemporaryHome = s"${GitBucketHome}/tmp" + /** * Substance directory of the repository. */ @@ -57,13 +59,18 @@ * Root of temporary directories for the upload file. */ def getTemporaryDir(sessionId: String): File = - new File(s"${GitBucketHome}/tmp/_upload/${sessionId}") + new File(s"${TemporaryHome}/_upload/${sessionId}") /** * Root of temporary directories for the specified repository. */ def getTemporaryDir(owner: String, repository: String): File = - new File(s"${GitBucketHome}/tmp/${owner}/${repository}") + new File(s"${TemporaryHome}/${owner}/${repository}") + + /** + * Root of plugin cache directory. Plugin repositories are cloned into this directory. + */ + def getPluginCacheDir(): File = new File(s"${TemporaryHome}/_plugins") /** * Temporary directory which is used to create an archive to download repository contents. diff --git a/src/main/twirl/admin/plugins/available.scala.html b/src/main/twirl/admin/plugins/available.scala.html new file mode 100644 index 0000000..0c5cf9a --- /dev/null +++ b/src/main/twirl/admin/plugins/available.scala.html @@ -0,0 +1,35 @@ +@(plugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main("Plugins"){ + @admin.html.menu("plugins"){ + @tab("available") +
+ + + + + + + @plugins.zipWithIndex.map { case (plugin, i) => + + + + + + } +
IDProviderDescription
+ + @plugin.id + @plugin.author@plugin.description
+ +
+ } +} + diff --git a/src/main/twirl/admin/plugins/tab.scala.html b/src/main/twirl/admin/plugins/tab.scala.html index 34d9056..ec3ebf6 100644 --- a/src/main/twirl/admin/plugins/tab.scala.html +++ b/src/main/twirl/admin/plugins/tab.scala.html @@ -2,6 +2,6 @@ @import context._