diff --git a/build.sbt b/build.sbt index 116715d..8768568 100644 --- a/build.sbt +++ b/build.sbt @@ -50,6 +50,7 @@ "org.cache2k" % "cache2k-all" % "1.0.0.CR1", "com.enragedginger" %% "akka-quartz-scheduler" % "1.6.0-akka-2.4.x" exclude("c3p0","c3p0"), "net.coobird" % "thumbnailator" % "0.4.8", + "com.github.zafarkhaja" % "java-semver" % "0.9.0", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "junit" % "junit" % "4.12" % "test", diff --git a/src/main/resources/plugins/gitbucket-emoji-plugin_2.12-4.4.0.jar b/src/main/resources/plugins/gitbucket-emoji-plugin_2.12-4.4.0.jar new file mode 100644 index 0000000..9f4fdec --- /dev/null +++ b/src/main/resources/plugins/gitbucket-emoji-plugin_2.12-4.4.0.jar Binary files differ diff --git a/src/main/resources/plugins/gitbucket-gist-plugin_2.12-4.9.0.jar b/src/main/resources/plugins/gitbucket-gist-plugin_2.12-4.9.0.jar new file mode 100644 index 0000000..b803b4e --- /dev/null +++ b/src/main/resources/plugins/gitbucket-gist-plugin_2.12-4.9.0.jar Binary files differ diff --git a/src/main/resources/plugins/gitbucket-notifications-plugin_2.12-1.0.0.jar b/src/main/resources/plugins/gitbucket-notifications-plugin_2.12-1.0.0.jar new file mode 100644 index 0000000..a82301a --- /dev/null +++ b/src/main/resources/plugins/gitbucket-notifications-plugin_2.12-1.0.0.jar Binary files differ diff --git a/src/main/resources/plugins/plugins.json b/src/main/resources/plugins/plugins.json new file mode 100644 index 0000000..2b6fc6f --- /dev/null +++ b/src/main/resources/plugins/plugins.json @@ -0,0 +1,41 @@ +[ + { + "id": "notifications", + "name": "Notifications Plugin", + "description": "Provides Notifications feature on GitBucket.", + "versions": [ + { + "version": "1.0.0", + "range": ">4.15.0-SNAPSHOT", + "file": "gitbucket-gist-notifications_2.12-1.0.0.jar" + } + ], + "default": true + }, + { + "id": "emoji", + "name": "Emoji Plugin", + "description": "Provides Emoji support for GitBucket.", + "versions": [ + { + "version": "4.4.0", + "range": ">=4.10.0", + "file": "gitbucket-emoji-plugin_2.12-4.4.0.jar" + } + ], + "default": true + }, + { + "id": "gist", + "name": "Gist Plugin", + "description": "Provides Gist feature on GitBucket.", + "versions": [ + { + "version": "4.9.0", + "range": ">=4.14.0", + "file": "gitbucket-gist-plugin_2.12-4.9.0.jar" + } + ], + "default": false + } +] diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index f9be729..7bc40ca 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -31,9 +31,8 @@ // Register controllers context.mount(new AnonymousAccessController, "/*") - PluginRegistry().getControllers.foreach { case (controller, path) => - context.mount(controller, path) - } + context.addFilter("pluginControllerFilter", new PluginControllerFilter) + context.getFilterRegistration("pluginControllerFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") context.mount(new IndexController, "/") context.mount(new ApiController, "/api/v3") diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index 9220853..7841721 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -6,7 +6,7 @@ import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService} import gitbucket.core.util.{AdminAuthenticator, Mailer} import gitbucket.core.ssh.SshServer -import gitbucket.core.plugin.PluginRegistry +import gitbucket.core.plugin.{PluginInfoBase, PluginRegistry, PluginRepository} import SystemSettingsService._ import gitbucket.core.util.Implicits._ import gitbucket.core.util.SyntaxSugars._ @@ -15,6 +15,10 @@ import io.github.gitbucket.scalatra.forms._ import org.apache.commons.io.{FileUtils, IOUtils} import org.scalatra.i18n.Messages +import com.github.zafarkhaja.semver.{Version => Semver} +import gitbucket.core.GitBucketCoreModule +import scala.collection.JavaConverters._ + class SystemSettingsController extends SystemSettingsControllerBase with AccountService with RepositoryService with AdminAuthenticator @@ -181,7 +185,71 @@ }) get("/admin/plugins")(adminOnly { - html.plugins(PluginRegistry().getPlugins()) + // Installed plugins + val enabledPlugins = PluginRegistry().getPlugins() + + val gitbucketVersion = Semver.valueOf(GitBucketCoreModule.getVersions.asScala.last.getVersion) + + // Plugins in the local repository + val repositoryPlugins = PluginRepository.getPlugins() + .filterNot { meta => + enabledPlugins.exists { plugin => plugin.pluginId == meta.id && + Semver.valueOf(plugin.pluginVersion).greaterThanOrEqualTo(Semver.valueOf(meta.latestVersion.version)) + } + }.map { meta => + (meta, meta.versions.reverse.find { version => gitbucketVersion.satisfies(version.range) }) + }.collect { case (meta, Some(version)) => + new PluginInfoBase( + pluginId = meta.id, + pluginName = meta.name, + pluginVersion = version.version, + description = meta.description + ) + } + + // Merge + val plugins = enabledPlugins.map((_, true)) ++ repositoryPlugins.map((_, false)) + + html.plugins(plugins, flash.get("info")) + }) + + post("/admin/plugins/_reload")(adminOnly { + PluginRegistry.reload(request.getServletContext(), loadSystemSettings(), request2Session(request).conn) + flash += "info" -> "All plugins were reloaded." + redirect("/admin/plugins") + }) + + post("/admin/plugins/:pluginId/:version/_uninstall")(adminOnly { + val pluginId = params("pluginId") + val version = params("version") + PluginRegistry().getPlugins() + .collect { case plugin if (plugin.pluginId == pluginId && plugin.pluginVersion == version) => plugin } + .foreach { _ => + PluginRegistry.uninstall(pluginId, request.getServletContext, loadSystemSettings(), request2Session(request).conn) + flash += "info" -> s"${pluginId} was uninstalled." + } + redirect("/admin/plugins") + }) + + post("/admin/plugins/:pluginId/:version/_install")(adminOnly { + val pluginId = params("pluginId") + val version = params("version") + /// TODO!!!! + PluginRepository.getPlugins() + .collect { case meta if meta.id == pluginId => (meta, meta.versions.find(_.version == version) )} + .foreach { case (meta, version) => + version.foreach { version => + // TODO Install version! + PluginRegistry.install( + new java.io.File(PluginHome, s".repository/${version.file}"), + request.getServletContext, + loadSystemSettings(), + request2Session(request).conn + ) + flash += "info" -> s"${pluginId} was installed." + } + } + redirect("/admin/plugins") }) diff --git a/src/main/scala/gitbucket/core/plugin/Plugin.scala b/src/main/scala/gitbucket/core/plugin/Plugin.scala index 0cb6a64..7f135e5 100644 --- a/src/main/scala/gitbucket/core/plugin/Plugin.scala +++ b/src/main/scala/gitbucket/core/plugin/Plugin.scala @@ -315,11 +315,17 @@ } /** - * This method is invoked in shutdown of plugin system. + * This method is invoked when the plugin system is shutting down. * If the plugin has any resources, release them in this method. */ def shutdown(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {} +// /** +// * This method is invoked when this plugin is uninstalled. +// * Cleanup database or any other resources in this method if necessary. +// */ +// def uninstall(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {} + /** * Helper method to get a resource from classpath. */ diff --git a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala index f06584e..dfd659a 100644 --- a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala +++ b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala @@ -2,6 +2,7 @@ import java.io.{File, FilenameFilter, InputStream} import java.net.URLClassLoader +import java.nio.file.{Files, Paths, StandardWatchEventKinds} import java.util.Base64 import javax.servlet.ServletContext @@ -9,6 +10,7 @@ import gitbucket.core.model.{Account, Issue} import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook import gitbucket.core.service.RepositoryService.RepositoryInfo +import gitbucket.core.service.SystemSettingsService import gitbucket.core.service.SystemSettingsService.SystemSettings import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.DatabaseConfig @@ -16,11 +18,13 @@ import io.github.gitbucket.solidbase.Solidbase import io.github.gitbucket.solidbase.manager.JDBCVersionManager import io.github.gitbucket.solidbase.model.Module +import org.apache.commons.io.FileUtils import org.slf4j.LoggerFactory import play.twirl.api.Html import scala.collection.mutable import scala.collection.mutable.ListBuffer +import com.github.zafarkhaja.semver.{Version => Semver} class PluginRegistry { @@ -39,10 +43,8 @@ private val repositoryHooks = new ListBuffer[RepositoryHook] private val issueHooks = new ListBuffer[IssueHook] - issueHooks += new gitbucket.core.util.Notifier.IssueHook() private val pullRequestHooks = new ListBuffer[PullRequestHook] - pullRequestHooks += new gitbucket.core.util.Notifier.PullRequestHook() private val repositoryHeaders = new ListBuffer[(RepositoryInfo, Context) => Option[Html]] private val globalMenus = new ListBuffer[(Context) => Option[Link]] @@ -185,7 +187,9 @@ private val logger = LoggerFactory.getLogger(classOf[PluginRegistry]) - private val instance = new PluginRegistry() + private var instance = new PluginRegistry() + + private var watcher: PluginWatchThread = null /** * Returns the PluginRegistry singleton instance. @@ -193,28 +197,91 @@ def apply(): PluginRegistry = instance /** + * Reload all plugins. + */ + def reload(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized { + shutdown(context, settings) + instance = new PluginRegistry() + initialize(context, settings, conn) + } + + /** + * Uninstall a specified plugin. + */ + def uninstall(pluginId: String, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized { + instance.getPlugins() + .collect { case plugin if plugin.pluginId == pluginId => plugin } + .foreach { plugin => +// try { +// plugin.pluginClass.uninstall(instance, context, settings) +// } catch { +// case e: Exception => +// logger.error(s"Error during uninstalling plugin: ${plugin.pluginJar.getName}", e) +// } + shutdown(context, settings) + plugin.pluginJar.delete() + instance = new PluginRegistry() + initialize(context, settings, conn) + } + } + + /** + * Install a plugin from a specified jar file. + */ + def install(file: File, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized { + FileUtils.copyFile(file, new File(PluginHome, file.getName)) + + shutdown(context, settings) + instance = new PluginRegistry() + initialize(context, settings, conn) + } + + private def listPluginJars(dir: File): Seq[File] = { + dir.listFiles(new FilenameFilter { + override def accept(dir: File, name: String): Boolean = name.endsWith(".jar") + }).toSeq.sortBy(_.getName).reverse + } + + /** * Initializes all installed plugins. */ - def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = { + def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized { val pluginDir = new File(PluginHome) val manager = new JDBCVersionManager(conn) - if(pluginDir.exists && pluginDir.isDirectory){ + // Clean installed directory + val installedDir = new File(PluginHome, ".installed") + if(installedDir.exists){ + FileUtils.deleteDirectory(installedDir) + } + installedDir.mkdir() + + if(pluginDir.exists && pluginDir.isDirectory) { pluginDir.listFiles(new FilenameFilter { override def accept(dir: File, name: String): Boolean = name.endsWith(".jar") - }).sortBy(_.getName).foreach { pluginJar => - val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader) + }).toSeq.sortBy(_.getName).reverse.foreach { pluginJar => + + val installedJar = new File(installedDir, pluginJar.getName) + FileUtils.copyFile(pluginJar, installedJar) + + logger.info(s"Initialize ${pluginJar.getName}") + val classLoader = new URLClassLoader(Array(installedJar.toURI.toURL), Thread.currentThread.getContextClassLoader) try { val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin] + val pluginId = plugin.pluginId + // Check duplication + instance.getPlugins().find(_.pluginId == pluginId).foreach { x => + throw new IllegalStateException(s"Plugin ${pluginId} is duplicated. ${x.pluginJar.getName} is available.") + } // Migration val solidbase = new Solidbase() solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*)) - // Check version + // Check database version val databaseVersion = manager.getCurrentVersion(plugin.pluginId) val pluginVersion = plugin.versions.last.getVersion - if(databaseVersion != pluginVersion){ + if (databaseVersion != pluginVersion) { throw new IllegalStateException(s"Plugin version is ${pluginVersion}, but database version is ${databaseVersion}") } @@ -225,39 +292,107 @@ pluginName = plugin.pluginName, pluginVersion = plugin.versions.last.getVersion, description = plugin.description, - pluginClass = plugin + pluginClass = plugin, + pluginJar = pluginJar, + classLoader = classLoader )) } catch { - case e: Throwable => { - logger.error(s"Error during plugin initialization: ${pluginJar.getAbsolutePath}", e) - } + case e: Throwable => logger.error(s"Error during plugin initialization: ${pluginJar.getName}", e) } } } + + if(watcher == null){ + watcher = new PluginWatchThread(context) + watcher.start() + } } - def shutdown(context: ServletContext, settings: SystemSettings): Unit = { - instance.getPlugins().foreach { pluginInfo => + def shutdown(context: ServletContext, settings: SystemSettings): Unit = synchronized { + instance.getPlugins().foreach { plugin => try { - pluginInfo.pluginClass.shutdown(instance, context, settings) + plugin.pluginClass.shutdown(instance, context, settings) } catch { case e: Exception => { - logger.error(s"Error during plugin shutdown", e) + logger.error(s"Error during plugin shutdown: ${plugin.pluginJar.getName}", e) } + } finally { + plugin.classLoader.close() } } } - } -case class Link(id: String, label: String, path: String, icon: Option[String] = None) +case class Link( + id: String, + label: String, + path: String, + icon: Option[String] = None +) + +class PluginInfoBase( + val pluginId: String, + val pluginName: String, + val pluginVersion: String, + val description: String +) case class PluginInfo( - pluginId: String, - pluginName: String, - pluginVersion: String, - description: String, - pluginClass: Plugin -) + override val pluginId: String, + override val pluginName: String, + override val pluginVersion: String, + override val description: String, + pluginClass: Plugin, + pluginJar: File, + classLoader: URLClassLoader +) extends PluginInfoBase(pluginId, pluginName, pluginVersion, description) + +class PluginWatchThread(context: ServletContext) extends Thread with SystemSettingsService { + import gitbucket.core.model.Profile.profile.blockingApi._ + import scala.collection.JavaConverters._ + + private val logger = LoggerFactory.getLogger(classOf[PluginWatchThread]) + + override def run(): Unit = { + val path = Paths.get(PluginHome) + if(!Files.exists(path)){ + Files.createDirectories(path) + } + val fs = path.getFileSystem + val watcher = fs.newWatchService + + val watchKey = path.register(watcher, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE, + StandardWatchEventKinds.OVERFLOW) + + logger.info("Start PluginWatchThread: " + path) + + try { + while (watchKey.isValid()) { + val detectedWatchKey = watcher.take() + val events = detectedWatchKey.pollEvents.asScala.filter(_.context.toString != ".installed") + if(events.nonEmpty){ + events.foreach { event => + logger.info(event.kind + ": " + event.context) + } + + gitbucket.core.servlet.Database() withTransaction { session => + logger.info("Reloading plugins...") + PluginRegistry.reload(context, loadSystemSettings(), session.conn) + logger.info("Reloading finished.") + } + } + detectedWatchKey.reset() + } + } catch { + case _: InterruptedException => watchKey.cancel() + } + + logger.info("Shutdown PluginWatchThread") + } + +} diff --git a/src/main/scala/gitbucket/core/plugin/PluginRepository.scala b/src/main/scala/gitbucket/core/plugin/PluginRepository.scala new file mode 100644 index 0000000..b509323 --- /dev/null +++ b/src/main/scala/gitbucket/core/plugin/PluginRepository.scala @@ -0,0 +1,41 @@ +package gitbucket.core.plugin + +import org.json4s._ +import gitbucket.core.util.Directory._ +import org.apache.commons.io.FileUtils + +object PluginRepository { + implicit val formats = DefaultFormats + + def parsePluginJson(json: String): Seq[PluginMetadata] = { + org.json4s.jackson.JsonMethods.parse(json).extract[Seq[PluginMetadata]] + } + + lazy val LocalRepositoryDir = new java.io.File(PluginHome, ".repository") + lazy val LocalRepositoryIndexFile = new java.io.File(LocalRepositoryDir, "plugins.json") + + def getPlugins(): Seq[PluginMetadata] = { + if(LocalRepositoryIndexFile.exists){ + parsePluginJson(FileUtils.readFileToString(LocalRepositoryIndexFile, "UTF-8")) + } else Nil + } + +} + +// Mapped from plugins.json +case class PluginMetadata( + id: String, + name: String, + description: String, + versions: Seq[VersionDef], + default: Boolean = false +){ + lazy val latestVersion: VersionDef = versions.last +} + +case class VersionDef( + version: String, + file: String, + range: String +) + diff --git a/src/main/scala/gitbucket/core/servlet/InitializeListener.scala b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala index 94dfc81..9e46f65 100644 --- a/src/main/scala/gitbucket/core/servlet/InitializeListener.scala +++ b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala @@ -1,25 +1,30 @@ package gitbucket.core.servlet -import java.io.File +import java.io.{File, FileOutputStream} import akka.event.Logging import com.typesafe.config.ConfigFactory import gitbucket.core.GitBucketCoreModule -import gitbucket.core.plugin.PluginRegistry +import gitbucket.core.plugin.{PluginRegistry, PluginRepository} import gitbucket.core.service.{ActivityService, SystemSettingsService} import gitbucket.core.util.DatabaseConfig import gitbucket.core.util.Directory._ +import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.JDBCUtil._ import gitbucket.core.model.Profile.profile.blockingApi._ import io.github.gitbucket.solidbase.Solidbase import io.github.gitbucket.solidbase.manager.JDBCVersionManager -import javax.servlet.{ServletContextListener, ServletContextEvent} -import org.apache.commons.io.FileUtils +import javax.servlet.{ServletContextEvent, ServletContextListener} + +import org.apache.commons.io.{FileUtils, IOUtils} import org.slf4j.LoggerFactory -import akka.actor.{Actor, Props, ActorSystem} +import akka.actor.{Actor, ActorSystem, Props} import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension +import com.github.zafarkhaja.semver.{Version => Semver} + import scala.collection.JavaConverters._ + /** * Initialize GitBucket system. * Update database schema and load plug-ins automatically in the context initializing. @@ -54,44 +59,11 @@ val manager = new JDBCVersionManager(conn) // Check version - val versionFile = new File(GitBucketHome, "version") - - if(versionFile.exists()){ - val version = FileUtils.readFileToString(versionFile, "UTF-8") - if(version == "3.14"){ - // Initialization for GitBucket 3.14 - logger.info("Migration to GitBucket 4.x start") - - // Backup current data - val dataMvFile = new File(GitBucketHome, "data.mv.db") - if(dataMvFile.exists) { - FileUtils.copyFile(dataMvFile, new File(GitBucketHome, "data.mv.db_3.14")) - } - val dataTraceFile = new File(GitBucketHome, "data.trace.db") - if(dataTraceFile.exists) { - FileUtils.copyFile(dataTraceFile, new File(GitBucketHome, "data.trace.db_3.14")) - } - - // Change form - manager.initialize() - manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0") - conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs => - manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION")) - } - conn.update("DROP TABLE PLUGIN") - versionFile.delete() - - logger.info("Migration to GitBucket 4.x completed") - - } else { - throw new Exception("GitBucket can't migrate from this version. Please update to 3.14 at first.") - } - } + checkVersion(manager, conn) // Run normal migration logger.info("Start schema update") - val solidbase = new Solidbase() - solidbase.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule) + new Solidbase().migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule) // Rescue code for users who updated from 3.14 to 4.0.0 // https://github.com/gitbucket/gitbucket/issues/1227 @@ -106,6 +78,9 @@ throw new IllegalStateException(s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}.") } + // Install bundled plugins + extractBundledPlugins(gitbucketVersion) + // Load plugins logger.info("Initialize plugins") PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn) @@ -117,7 +92,74 @@ scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity") } + private def checkVersion(manager: JDBCVersionManager, conn: java.sql.Connection): Unit = { + logger.info("Check version") + val versionFile = new File(GitBucketHome, "version") + if(versionFile.exists()){ + val version = FileUtils.readFileToString(versionFile, "UTF-8") + if(version == "3.14"){ + // Initialization for GitBucket 3.14 + logger.info("Migration to GitBucket 4.x start") + + // Backup current data + val dataMvFile = new File(GitBucketHome, "data.mv.db") + if(dataMvFile.exists) { + FileUtils.copyFile(dataMvFile, new File(GitBucketHome, "data.mv.db_3.14")) + } + val dataTraceFile = new File(GitBucketHome, "data.trace.db") + if(dataTraceFile.exists) { + FileUtils.copyFile(dataTraceFile, new File(GitBucketHome, "data.trace.db_3.14")) + } + + // Change form + manager.initialize() + manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0") + conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs => + manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION")) + } + conn.update("DROP TABLE PLUGIN") + versionFile.delete() + + logger.info("Migration to GitBucket 4.x completed") + + } else { + throw new Exception("GitBucket can't migrate from this version. Please update to 3.14 at first.") + } + } + } + + private def extractBundledPlugins(gitbucketVersion: String): Unit = { + logger.info("Extract bundled plugins") + val cl = Thread.currentThread.getContextClassLoader + try { + using(cl.getResourceAsStream("plugins/plugins.json")){ pluginsFile => + val pluginsJson = IOUtils.toString(pluginsFile, "UTF-8") + + FileUtils.forceMkdir(PluginRepository.LocalRepositoryDir) + FileUtils.write(PluginRepository.LocalRepositoryIndexFile, pluginsJson, "UTF-8") + + val plugins = PluginRepository.parsePluginJson(pluginsJson) + plugins.foreach { plugin => + plugin.versions.sortBy { x => Semver.valueOf(x.version) }.reverse.zipWithIndex.foreach { case (version, i) => + val file = new File(PluginRepository.LocalRepositoryDir, version.file) + if(!file.exists) { + logger.info(s"Copy ${plugin} to ${file.getAbsolutePath}") + FileUtils.forceMkdirParent(file) + using(cl.getResourceAsStream("plugins/" + version.file), new FileOutputStream(file)){ case (in, out) => IOUtils.copy(in, out) } + + if(plugin.default && i == 0){ + logger.info(s"Enable ${file.getName} in default") + FileUtils.copyFile(file, new File(PluginHome, version.file)) + } + } + } + } + } + } catch { + case e: Exception => logger.error("Error in extracting bundled plugin", e) + } + } override def contextDestroyed(event: ServletContextEvent): Unit = { // Shutdown Quartz scheduler @@ -146,4 +188,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/main/scala/gitbucket/core/servlet/PluginControllerFilter.scala b/src/main/scala/gitbucket/core/servlet/PluginControllerFilter.scala new file mode 100644 index 0000000..442841b --- /dev/null +++ b/src/main/scala/gitbucket/core/servlet/PluginControllerFilter.scala @@ -0,0 +1,37 @@ +package gitbucket.core.servlet + +import javax.servlet._ +import javax.servlet.http.HttpServletRequest + +import gitbucket.core.plugin.PluginRegistry + +class PluginControllerFilter extends Filter { + + private var filterConfig: FilterConfig = null + + override def init(filterConfig: FilterConfig): Unit = { + this.filterConfig = filterConfig + } + + override def destroy(): Unit = { + PluginRegistry().getControllers().foreach { case (controller, _) => + controller.destroy() + } + } + + override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = { + val controller = PluginRegistry().getControllers().find { case (_, path) => + val requestUri = request.asInstanceOf[HttpServletRequest].getRequestURI + path.endsWith("/*") && requestUri.startsWith(path.replaceFirst("/\\*$", "/")) + } + + controller.map { case (controller, _) => + if(controller.config == null){ + controller.init(filterConfig) + } + controller.doFilter(request, response, chain) + }.getOrElse{ + chain.doFilter(request, response) + } + } +} diff --git a/src/main/scala/gitbucket/core/util/Notifier.scala b/src/main/scala/gitbucket/core/util/Notifier.scala index 3e8d39c..0c7d861 100644 --- a/src/main/scala/gitbucket/core/util/Notifier.scala +++ b/src/main/scala/gitbucket/core/util/Notifier.scala @@ -1,10 +1,9 @@ package gitbucket.core.util -import gitbucket.core.model.{Session, Issue, Account} +import gitbucket.core.model.{Session, Account} import gitbucket.core.model.Profile.profile.blockingApi._ -import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, SystemSettingsService} +import gitbucket.core.service.SystemSettingsService import gitbucket.core.servlet.Database -import gitbucket.core.view.Markdown import scala.concurrent._ import scala.util.{Success, Failure} @@ -31,125 +30,6 @@ case settings if (settings.notification && settings.useSMTP) => new Mailer(settings.smtp.get) case _ => new MockMailer } - - - // TODO This class is temporary keeping the current feature until Notifications Plugin is available. - class IssueHook extends gitbucket.core.plugin.IssueHook - with RepositoryService with AccountService with IssuesService { - - override def created(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = { - Notifier().toNotify( - subject(issue, r), - message(issue.content getOrElse "", r)(content => s""" - |$content
- |--
- |View it on GitBucket - """.stripMargin) - )(recipients(issue)) - } - - override def addedComment(commentId: Int, content: String, issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = { - Notifier().toNotify( - subject(issue, r), - message(content, r)(content => s""" - |$content
- |--
- |View it on GitBucket - """.stripMargin) - )(recipients(issue)) - } - - override def closed(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = { - Notifier().toNotify( - subject(issue, r), - message("close", r)(content => s""" - |$content #${issue.issueId} - """.stripMargin) - )(recipients(issue)) - } - - override def reopened(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = { - Notifier().toNotify( - subject(issue, r), - message("reopen", r)(content => s""" - |$content #${issue.issueId} - """.stripMargin) - )(recipients(issue)) - } - - - protected def subject(issue: Issue, r: RepositoryService.RepositoryInfo): String = - s"[${r.owner}/${r.name}] ${issue.title} (#${issue.issueId})" - - protected def message(content: String, r: RepositoryService.RepositoryInfo)(msg: String => String)(implicit context: Context): String = - msg(Markdown.toHtml( - markdown = content, - repository = r, - enableWikiLink = false, - enableRefsLink = true, - enableAnchor = false, - enableLineBreaks = false - )) - - protected val recipients: Issue => Account => Session => Seq[String] = { - issue => loginAccount => implicit session => - ( - // individual repository's owner - issue.userName :: - // group members of group repository - getGroupMembers(issue.userName).map(_.userName) ::: - // collaborators - getCollaboratorUserNames(issue.userName, issue.repositoryName) ::: - // participants - issue.openedUserName :: - getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) - ) - .distinct - .withFilter ( _ != loginAccount.userName ) // the operation in person is excluded - .flatMap ( - getAccountByUserName(_) - .filterNot (_.isGroupAccount) - .filterNot (LDAPUtil.isDummyMailAddress) - .map (_.mailAddress) - ) - } - } - - // TODO This class is temporary keeping the current feature until Notifications Plugin is available. - class PullRequestHook extends IssueHook with gitbucket.core.plugin.PullRequestHook { - override def created(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = { - val url = s"${context.baseUrl}/${r.owner}/${r.name}/pull/${issue.issueId}" - Notifier().toNotify( - subject(issue, r), - message(issue.content getOrElse "", r)(content => s""" - |$content
- |View, comment on, or merge it at:
- |$url - """.stripMargin) - )(recipients(issue)) - } - - override def addedComment(commentId: Int, content: String, issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = { - Notifier().toNotify( - subject(issue, r), - message(content, r)(content => s""" - |$content
- |--
- |View it on GitBucket - """.stripMargin) - )(recipients(issue)) - } - - override def merged(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = { - Notifier().toNotify( - subject(issue, r), - message("merge", r)(content => s""" - |$content #${issue.issueId} - """.stripMargin) - )(recipients(issue)) - } - } - } class Mailer(private val smtp: Smtp) extends Notifier { diff --git a/src/main/twirl/gitbucket/core/admin/plugins.scala.html b/src/main/twirl/gitbucket/core/admin/plugins.scala.html index 0186e49..9f37b45 100644 --- a/src/main/twirl/gitbucket/core/admin/plugins.scala.html +++ b/src/main/twirl/gitbucket/core/admin/plugins.scala.html @@ -1,18 +1,32 @@ -@(plugins: List[gitbucket.core.plugin.PluginInfo])(implicit context: gitbucket.core.controller.Context) +@(plugins: List[(gitbucket.core.plugin.PluginInfoBase, Boolean)], info: Option[Any])(implicit context: gitbucket.core.controller.Context) @gitbucket.core.html.main("Plugins"){ @gitbucket.core.admin.html.menu("plugins") { -

Installed plugins

- + @gitbucket.core.helper.html.information(info) +
+ +
+

Plugins

@if(plugins.size > 0) { - @plugins.map { plugin => + @plugins.map { case (plugin, enabled) =>
-
@plugin.pluginName
+
+ @if(enabled){ +
+ +
+ } else { +
+ +
+ } + @plugin.pluginName +
@@ -38,3 +52,16 @@ } } } +