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